Compass Project Updated to Angular 16, Standalone Components

After reading up on Angular Forms (both template-driven and reactive) I was ready to switch gears for some hands-on practice of what I’ve recently learned. My only Angular practice project so far is my Compass app. I couldn’t think of a reasonable way to practice Angular reactive forms with it, but I could practice a few other new learnings.

Angular 16 Upgrade

Every major Angular version upgrade is accompanied by a lot of information. Starting with the broadest strokes on the Angular Blog “Angular v16 is here!“, then more details in Angular documentation under “Updates and releases” as “Update Angular to v16” which points to an Angular Update Guide app that will list all the nuts and bolts details we should watch out for.

My compass app is very simple, so I get to practice Angular version upgrade on easy mode. Before I ran the update script, though, I thought I’d take a snapshot of my app size to see how it is impacted by upgrade.

Initial Chunk Files           | Names         |  Raw Size | Estimated Transfer Size
main.0432b89ce9f334d0.js      | main          | 642.10 kB |               145.70 kB
polyfills.342580026a9ebec0.js | polyfills     |  33.08 kB |                10.65 kB
runtime.7c1518bc3d8e48a2.js   | runtime       | 892 bytes |               513 bytes
styles.e5365f8304590c7a.css   | styles        |  51 bytes |                45 bytes

                              | Initial Total | 676.11 kB |               156.89 kB

Build at: 2023-05-23T23:37:24.621Z - Hash: a00cb4a3df82243e - Time: 22092ms

After running “ng update @angular/cli @angular/core“, Compass was up to Angular 16.

Initial Chunk Files           | Names         |  Raw Size | Estimated Transfer Size
main.6fd12210225d0aec.js      | main          | 645.17 kB |               146.60 kB
polyfills.f00f35de5fea72bd.js | polyfills     |  32.98 kB |                10.62 kB
runtime.7c1518bc3d8e48a2.js   | runtime       | 892 bytes |               513 bytes
styles.e5365f8304590c7a.css   | styles        |  51 bytes |                45 bytes

                              | Initial Total | 679.08 kB |               157.76 kB

Build at: 2023-05-23T23:50:05.468Z - Hash: f595c7c43b5422f4 - Time: 29271ms

Looks like it grew by 3 kilobytes, which is hard to complain about when it is 0.5% of app size.

Standalone Components

I then converted Compass components to become standalone components. Following the “Migrate an existing Angular project to standalone” guide, most of the straightforward conversion can be accomplished by running “ng generate @angular/core:standalone” three times. Each pass converts a different aspect of the project (convert components to standalone, remove vestigial NgModule, application bootstrap for standalone API) and we have an opportunity to verify our app still works.

Initial Chunk Files           | Names         |  Raw Size | Estimated Transfer Size
main.39226bf17498cc2d.js      | main          | 643.58 kB |               146.35 kB
polyfills.f00f35de5fea72bd.js | polyfills     |  32.98 kB |                10.62 kB
runtime.7c1518bc3d8e48a2.js   | runtime       | 892 bytes |               513 bytes
styles.e5365f8304590c7a.css   | styles        |  51 bytes |                45 bytes

                              | Initial Total | 677.48 kB |               157.52 kB

Since Compass is a pretty simple app, eliminating NgModule didn’t change very much. All the same things (declare dependencies, etc.) still had to be done, they just live in different places. From a code size perspective, eliminating NgModule shrunk app size down by about 1.5 kilobytes, reclaiming about half of the minimal growth from converting to v16.

Remove Router

Compass is a very simple app that really didn’t use the Angular router for any of its advanced capabilities. Heck, with a single URL it didn’t even use any Angular router capability. But as a beginner, copying and pasting code from tutorials without fully understanding everything, I didn’t know that at the time. Now I know enough to recognize the router portions of the app (thanks to standalone components code lab) I could go in and remove router from Compass.

Initial Chunk Files           | Names         |  Raw Size | Estimated Transfer Size
main.4a58a43e0cb90db0.js      | main          | 565.24 kB |               128.77 kB
polyfills.f00f35de5fea72bd.js | polyfills     |  32.98 kB |                10.62 kB
runtime.7c1518bc3d8e48a2.js   | runtime       | 892 bytes |               513 bytes
styles.e5365f8304590c7a.css   | styles        |  51 bytes |                45 bytes

                              | Initial Total | 599.14 kB |               139.94 kB

Build at: 2023-05-24T00:30:36.609Z - Hash: 155b70d2931a3e06 - Time: 18666ms

Ah, now we’re talking. App size shrunk by about 77 kilobytes, quite significant relative to other changes.

Fix PWA Service Worker

And finally, I realized my mistake when playing with turning Compass into a PWA (Progressive Web App): I never told it anything about the deployment server. By default, a PWA assumes the Angular app lives at the root of the URL. My Compass web app is hosted via GitHub Pages at https://roger-random.github.io/compass, which is not the root of the URL. (That would be https://roger-random.github.io) In order for path resolution to work correctly, I had to pass in the path information via --base-href parameter for ng build and ng deploy. Once I started doing that (I updated my npm scripts to make it easier) I no longer see HTTP 404 errors in PWA service worker status page.

I’m happy with these improvements. I expect my Compass web app project will continue to improve alongside my understanding of Angular. The next step in that journey is to dive back into the Angular Signals code lab.


Source code for this project is publicly available on GitHub.

Compass Project Now a PWA

The full scope of PWA (Progressive Web App) is too much for me to comprehend with my limited web development skills. Fortunately, I don’t need to. PWA helper libraries exist, including some tailored to specific web frameworks. Reading over a developer guide for Angular framework’s PWA helper, I understood it offers basic PWA service worker functionality without much effort. Naturally, I had to give it a try and see how it turns my Compass project, a simple practice Angular web app, into a PWA.

Following instructions, I ran “ng add @angular/pwa” in my project and looked over the changes it made. As expected, there were several modifications to configuration JSON files on top of adding one of their own ngsw-config.json. Eight placeholder icon PNG files (showing the Angular logo) were added to the source tree, each representing a different resolution from 72×72 up to 512×512. Notably, the only TypeScript source code change was in app.module.ts to register service worker file ngsw-worker.js, and that service worker file is not part of the source tree where we can enhance it. And if there’s an extension mechanism, I don’t see it. This tells me behavioral changes are limited to what we can affect via settings in ngsw-config.json. For app authors that want to go beyond, they’d have to write their own PWA service worker from scratch.

But I don’t have any ambition of code changes in this first run. I replaced those placeholder icon PNG files. Change them from the Angular logo to a crude compass I whipped up in a few minutes. Plus a few edits to the application name and cosmetic theme color. (Duplicated in two locations: the .webmanifest file and in index.html.) I built the results, deployed to GitHub Pages, and brought it up on my Android phone Chrome browser to see if anything looks different.

Good news: Android Chrome recognized the site is available as a PWA and offered the option to install it. (Adding yet another browser interface bar to the clutter…) I clicked install, and my icon in all its crudeness was added to my Android home screen.

Bad news: I tapped the icon, saw the loading splash screen (with an even bigger version of my crude icon in the middle) and then… nothing. It was stuck at the splash screen.

I killed the app and tried a second time, which was successful: my compass needle came up on screen after a brief pause. (1-2 seconds at most.) I returned to home screen, tapped the icon again, and it came up instantly. Implying the app was suspended and resumed.

A few hours later, I tapped the icon again. It was apparently no longer suspended and no longer available for resume, because I saw the splash screen again and it got stuck again. Killing the app and immediately trying again was successful.

The observed pattern is thus: Initial launch would get stuck at the splash screen due to an undiagnosed issue. Killing the stuck app and immediately relaunching would be successful. Suspend and resume works, but if a suspended app is terminated, we’re back to the initial launch problem upon relaunch.

Since it wasn’t possible to change any code in @angular/pwa I’m not sure what I did wrong. Following instructions for “Debugging the Angular service worker” I headed over to the debug endpoint ngsw/state where I saw it couldn’t download favicon.ico from the server. (Full debug output error message below.) I could see the file exists on the server, so I don’t understand why this PWA service worker could not download it. At the moment I’m at a loss as to how to diagnose this further. For today I’m going to declare a partial success and move on.

[UPDATE: I’ve figured it out. By default the PWA service worker assumes the app was deployed to server root, which in this case was incorrect. Since it is hosted at https://roger-random.github.io/compass/ I needed to run “ng deploy --base-href /compass/“]


Source code for this project is publicly available on GitHub

Appendix

Full error from https://roger-random.github.io/compass/ngsw/state

NGSW Debug Info:

Driver version: 15.2.8
Driver state: EXISTING_CLIENTS_ONLY (Degraded due to failed initialization: Failed to retrieve hashed resource from the server. (AssetGroup: app | URL: /favicon.ico)
Error: Failed to retrieve hashed resource from the server. (AssetGroup: app | URL: /favicon.ico)
    at PrefetchAssetGroup.cacheBustedFetchFromNetwork (https://roger-random.github.io/compass/ngsw-worker.js:479:17)
    at async PrefetchAssetGroup.fetchFromNetwork (https://roger-random.github.io/compass/ngsw-worker.js:449:19)
    at async PrefetchAssetGroup.fetchAndCacheOnce (https://roger-random.github.io/compass/ngsw-worker.js:428:21)
    at async https://roger-random.github.io/compass/ngsw-worker.js:528:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9)
Latest manifest hash: ecc53033977c5c6ddfd3565d7c2bc201432c42ff
Last update check: 37s199u

=== Version ecc53033977c5c6ddfd3565d7c2bc201432c42ff ===

Clients: 

=== Idle Task Queue ===
Last update tick: 42s670u
Last update run: 37s668u
Task queue:


Debug log:

[37s320u] Error(Failed to retrieve hashed resource from the server. (AssetGroup: app | URL: /favicon.ico), Error: Failed to retrieve hashed resource from the server. (AssetGroup: app | URL: /favicon.ico)
    at PrefetchAssetGroup.cacheBustedFetchFromNetwork (https://roger-random.github.io/compass/ngsw-worker.js:479:17)
    at async PrefetchAssetGroup.fetchFromNetwork (https://roger-random.github.io/compass/ngsw-worker.js:449:19)
    at async PrefetchAssetGroup.fetchAndCacheOnce (https://roger-random.github.io/compass/ngsw-worker.js:428:21)
    at async https://roger-random.github.io/compass/ngsw-worker.js:528:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9) Error occurred while updating to manifest 271c8a71bf2acb8075fc93af554137c2e15365f2
[37s229u] Error(Failed to retrieve hashed resource from the server. (AssetGroup: app | URL: /favicon.ico), Error: Failed to retrieve hashed resource from the server. (AssetGroup: app | URL: /favicon.ico)
    at PrefetchAssetGroup.cacheBustedFetchFromNetwork (https://roger-random.github.io/compass/ngsw-worker.js:479:17)
    at async PrefetchAssetGroup.fetchFromNetwork (https://roger-random.github.io/compass/ngsw-worker.js:449:19)
    at async PrefetchAssetGroup.fetchAndCacheOnce (https://roger-random.github.io/compass/ngsw-worker.js:428:21)
    at async https://roger-random.github.io/compass/ngsw-worker.js:528:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9) initializeFully for ecc53033977c5c6ddfd3565d7c2bc201432c42ff
[37s110u] Error(Failed to retrieve hashed resource from the server. (AssetGroup: app | URL: /favicon.ico), Error: Failed to retrieve hashed resource from the server. (AssetGroup: app | URL: /favicon.ico)
    at PrefetchAssetGroup.cacheBustedFetchFromNetwork (https://roger-random.github.io/compass/ngsw-worker.js:479:17)
    at async PrefetchAssetGroup.fetchFromNetwork (https://roger-random.github.io/compass/ngsw-worker.js:449:19)
    at async PrefetchAssetGroup.fetchAndCacheOnce (https://roger-random.github.io/compass/ngsw-worker.js:428:21)
    at async https://roger-random.github.io/compass/ngsw-worker.js:528:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9
    at async https://roger-random.github.io/compass/ngsw-worker.js:519:9) Error occurred while updating to manifest 271c8a71bf2acb8075fc93af554137c2e15365f2

Compass Project Version 1.0 Complete

Along with source code, GitHub code repositories have an “Issues” section for tracking problems. I’ve logged the outstanding problems and their workarounds for my Compass project. Since fixing them are outside of my control, I am going to declare version 1.0 complete. Now to wrap up before moving on to something else.

The primary goal of this project was to practice working with Angular framework, and that goal is a success. I knew going in that Angular wouldn’t be the best tool for the job: its architecture was designed to handle much larger and more complex web apps than my Compass. But I wanted a chance to practice Angular with something small, and Compass succeeded at that.

An entirely expected consequence of my initial decision is a web app far larger than it needs to be. I knew from earlier experiments a bare-bones Angular app doing nothing more than <body><h1>Hello World</h1></body> start at over 200 kilobytes. I wasn’t too impressed by that, but as a practical matter I accept this is not considered huge in today’s web world. I found an online tool https://bundlescanner.com/ which lists download sizes for the given URL, and most of the sites I visit weigh in at several megabytes. Even the legendarily lightweight Google home page downloads over 300 kilobytes. Judged in that context, 200 kilobytes of overhead are unlikely to be a dealbreaker by themselves. Not even in space-constrained situations, thanks to the fact it compresses very well.

Since it’s such a small app, I didn’t touch upon very much beyond Angular core. I created just two components, one (compass-needle) inside the other (capability-check) so there was no routing at all. (It is literally a ‘single-page application’). I created two services who distributed information via RxJS, but I didn’t need to deal with server communication over a network. It’s good to start small to get over beginner hurdles. My next Angular practice project can be more complex to expand my exploration frontier.

Unrelated to Angular, one thing I knew I needed to practice — and I got lots of it here — was CSS layout. This was the first project where I used media queries to react to portrait vs. landscape orientations, and it was great practice towards more responsive layouts in future projects. I know people can do great things with CSS, but I don’t count myself among them. I foresee a lot more hair-pulling in the future as I struggle to declare CSS rules to create what I have in my mind, but that’s the journey I know I need to take.

Compass Web App Workarounds

After giving my Compass web app the ability to go full screen, it’s working pretty much as I had imagined when I started this project. Except, of course, for the two problems that I believe to be browser bugs outside of my control. I’ve always known that real-world web projects have a lot of ugly workarounds for problems hiding across browser implementations, but I had thought I could avoid that by targeting a single browser. (Chrome on Android with experimental magnetometer API.) Sadly, no such luck. It is time to get hacking.

The vertical text issue is merely cosmetic and the easiest to fix. I want sideways-lr but it doesn’t work. Fortunately, vertical-lr is also a vertical text layout, just rotated in the opposite direction from the one I wanted. Because I only have a single line of text, adding transform: rotate(180deg) was enough to get the result I wanted. I believe there would be additional complications if there were more than one line of text, but that’s a problem I don’t have to deal with today. I opened a GitHub issue to track removing this workaround, but as a practical matter, there’s no real harm leaving this workaround in place even if sideways-lr starts working correctly.

The same could not be said of the magnetometer {referenceFrame: 'screen'} problem, where landscape-left receives the result for landscape-right and vice versa. While the workaround is the same, adding transform: rotate(180deg) to flip things around, I can’t leave this workaround the same way as I could vertical text. As soon as the upstream browser bug is fixed, my workaround rotation would cause the display to be wrong again. And even worse, there’s no way for me to determine within the app whether the workaround is applicable. I couldn’t issue a CSS media query for whether this bug is fixed! I don’t know of a graceful way to handle this, but at least I’ve opened an issue for it as well.

And finally for completeness, I opened an issue tracking the fact magnetometer API is a preview experiment. Because it is likely to change, this app could stop working at any time. I expect that magnetometer API would mature to become available across multiple platforms, not just Chrome on Android. Once adoption broadens, this app needs to be updated from experimental to standard API.

Compass Web App Going Full Screen

HTML started as a way to display text within a given width and whatever height is needed to accommodate that content, scrolling if necessary. My desire to create a single-page layout without scrolling feels like a perpetual fight to put a square peg in a round hole. I’ve seen layouts I like out in the world, so I believe it’s only a matter of practice to master all the applicable techniques. A tool I wanted to add to my toolbox is the ability to make my web app go full screen.

When my web app first loads up on Chrome browser for Android, I declared CSS layout to work within full viewport width and height. However, Chrome doesn’t actually show my app on the full viewport, because it has a lot of user interfaces in the way. A bar up top has the address and other related buttons, and a bar at the bottom has my open tabs and additional controls. Together they consume more than one third of overall screen space, far too much for tasks not related to my web app. Fortunately, this situation is only temporary.

As soon as I tap my app, both bars retreat. A small bar is left up top for the phone icons (clock, signal strength, battery) and a bar below reminding me I could drag to unveil more controls. I estimate they consume approximately one eighths of overall screen space. An improvement, but I can do even better by going full screen.

MDN page on fullscreen API makes it seem pretty straightforward, at least for updated browsers that support the official standard. The complication comes from its history: Going full screen was such a desirable browser feature that they were available in vendor-specific ways before they were standardized. To support such browsers, MDN pointed to the fscreen library which packaged all the prefixed variations together. Fortunately, a personal project like my Compass web app doesn’t need to worry about legacy browser support. Besides, they probably wouldn’t have the magnetometer API anyway.

After adding a button for triggering fullscreen request, I can make my app full screen and free of all browser UI. It doesn’t necessarily mean I have the entire screen, though. I noticed that on my phone (Pixel 7) my app was prohibited from the screen region where a hole was punched for the front-facing camera. When my app is fullscreen, that area is filled with black so at least it is not distracting even if it was unusable. Native Android apps can request to render into this “display cutout” area, but as far as I can tell browser apps are out of luck. That’s a relatively trivial detail I can ignore, but I have to devise workarounds for other problems out of my control.


Source code for this project is publicly available on GitHub

CSS Beginner Struggles: aspect-ratio and height

Reviewing CSS from web.dev’s “Learn CSS!” course provided a refresher on a lot of material and also introduced me to new material I hadn’t seen before. I had hoped for a bit of “aha” insight to help me with CSS struggles in my project, but that didn’t happen. The closest was a particular piece of information (Flexbox for laying out along one dimension, Grid for two dimensions) that told me I’m on the right track using Flexbox.

A recurring theme with my CSS frustration is the fact height and width are not treated the same way in HTML layout. I like to think of them as peers, two equal and orthogonal dimensions, but that’s not how things work here. It traces back to HTML fundamentals of laying out text for reading text in a left-to-right, top-to bottom languages like English. Like a typesetter, the layout is specified in terms of width. Column width, margin width, etc. Those were the parameters that fed into layout. Height of a paragraph is then determined by the length of text that could fit within specified width. Thus, height is an output result, not an input parameter, of the layout process.

For my Compass web app, I had a few text elements I knew I wanted to lay out. Header, footer, sensor values, etc. After they have all been allocated screen real estate, I wanted my compass needle to be the largest square that could fit within the remaining space. That last part is the problem: while we have ways to denote “all remaining space” for width, there’s no such equivalent for height because height is a function of width and content. This results in unresolvable circular logic when my content (square compass) is a function of height, but the height is a function of my content.

I could get most of the way to my goal with liberal application of “height: 100%” in my CSS rules. It does not appear to inherit/cascade, so I have to specify “height: 100%” on every element down the DOM hierarchy. If I don’t, height of that element collapses to zero because my compass doesn’t have an inherent height of its own.

Once I get to my compass, I could declare it to be a square with aspect-ratio. But when I did so, I find that aspect-ratio does its magic by changing element height to satisfy specified aspect ratio. When my remaining space is wider than it is tall, aspect-ratio expands height so it matches width. This is consistent with how the rest of HTML layout treats width vs. height, and it accomplishes the specified aspect ratio. But now it is too tall to fit within remaining space!

Trying to reign that in, I played with “height: 100%“, “max-height: 100%“, and varying combinations of similar CSS rules. They could affect CSS-specified height values, but seems to have no effect on height change from aspect-ratio. Setting aspect-ratio means height is changed to fit available width and I found no way to declare the reverse in CSS: change width to fit within available height.

From web.dev I saw Codepen.io offered ability to have code snippets in a webpage, so here’s a test to see how it works on my own blog. I pulled the HTML, CSS, and minimal JavaScript representing a Three.js <canvas> into a pen so I could fiddle with this specific problem independent of the rest of the app. I think I’ve embedded it below but here’s a link if the embed doesn’t work.

After preserving a snapshot of my headache in Codepen, I returned to Compass app which still had a problem that needed solving. Unable to express my intent via CSS, I turned to code. I abandoned using aspect-ratio and resized my Three.js canvas to a square whose size is calculated via:

Math.floor(Math.min(clientWidth, clientHeight));

Taking width or height, whichever is smaller, and then rounding down. I have to round down to the nearest whole number otherwise scroll bars pop up, and I don’t want scroll bars. I hate solving a layout problem with code, but it’ll have to do for now. Hopefully sometime in the future I will have a better grasp of CSS and can write the proper stylesheet to accomplish my goal. In the meantime, I look for other ways to make layout more predictable such as making my app full screen.


The source code for my project is publicly available on GitHub, though it no longer uses aspect-ratio as per the workaround described at the end of this poist.

Compass Web App in Landscape Exposed Browser Bugs

I got my Compass web app deployed to GitHub Pages and addressed some variations in browser rendering. Once it was working consistently enough across browsers, I tackled the next challenge: creating CSS layout for landscape as well as portrait orientations. Built solely around media query for orientation, it was a great exercise for me to build experience with CSS layouts. Most of my challenges can be chalked up to beginner’s awkwardness, but there were two factors that seemed to be beyond my control.

Sideways Text

While in landscape orientation, I wanted my headline banner to be rotated in order to occupy less screen real estate. This is a common enough desire that CSS has provision for this via writing-mode. My problem is that my desired direction (top of word pointing to screen left) doesn’t seem to work, I could only get vertical text in the opposite direction (top of word pointing to screen right, 180 degrees from desired.) The MDN page for writing-mode had an example indicating it’s not my mistake. It has a table of examples in markup followed by two renderings. A bitmap showing expected behavior (here with my desired output circled in red):

And then the markup as rendered by Chrome 112 (with different behavior circled in red.)

Looking for a workaround, I investigated CSS rotate transform. The good news is that I could get text rotated in the direction I want with rotate(-90deg). The bad news is that only happens after size layout. Meaning my header bar is the sized as if the header text was not rotated, which is very wide and thus defeating the objective of occupying less screen real estate.

I guess I can’t get the layout I want until some bugs are fixed or until I find a different workaround. Right now the least-bad alternative is to use writing-mode vertical-lr, which rotates text the wrong way from what I wanted but at least it is vertical and compact.

Magnetometer Reference Frame

Landscape mode uncovered another browser issue. When initializing the magnetometer object, we could specify the coordinate reference frame. Two values are supported: “device” is fixed regardless of device orientation, and “screen” will automatically transform coordinate space to match screen orientation. The good news is that “screen” does perform a coordinate transform while in landscape mode, the bad news is the transform is backwards: each landscape orientation gives information appropriate to the other landscape orientation.

For reference, here’s my app in portrait mode and the compass needle pointing roughly north. For a phone, this is the natural orientation where “device” and “screen” would give the same information.

After rotating the phone, Chrome browser rotates my app to landscape orientation as expected. Magnetometer coordinates were also transformed, but it is pointing the wrong way!

Still Lots to Learn

These two issues are annoying because they are out of my control, but they were only a minority of the problems I encountered trying to make my little app work in landscape mode. Vast majority of the mistakes were of my own making, as I learned how to use CSS for my own layout purposes. Hands-on time makes concepts concrete, and such experience helps me when I go back to review documentation.


Source code for this project is publicly available on GitHub

Compass Web App Browser Variations

Once deployed to GitHub Pages (made easier by moving the project into its own GitHub repository) I could easily try my web app across more devices and browsers. This compass web app only really works on my Android devices with magnetometers, but the page would come up with placeholder data on every modern browser. And naturally, there are variations between browsers. The differences on iOS Safari weren’t surprising, but I was surprised at the differences between Microsoft Edge and Google Chrome as they both purportedly used the same Chromium engine.

The first and most obvious difference are update rates. All browsers would show compass needle moving in response to either real or placeholder data, but the update rate varies. On Microsoft Edge, the update rate would be on par with Chrome but would drastically slow down after several (~5) seconds without user interactivity. If I touch the needle, response rate picks back up for another few seconds before slowing down. I suspect this is a consequence of aggressive throttling of animation and/or timers in the goal of saving power.

Another difference are in page updates. One example on my is “{{magX | number:'1.2-2'}}” which is supposed to print the value of magX property to two decimal point precision. (Y and Z are handled the same way.) I update magX every time data is received, but that isn’t necessarily shown on screen. Chrome shows as expected but Edge never updates. There’s something different about how Angular runs its change detection between these two browsers. Until I understand how to work within the system, I can work around the problem by manually calling ChangeDetectorRef.detectChanges() to notify that new numbers need to be picked up.

Once I had portrait mode working more or less as intended, I started looking into landscape mode and found… uh… many more learning opportunities.


Source code for this project is publicly available on GitHub

Compass Web App Project Gets Own Repository

Once I got Three.js and Magnetometer API up and running within the context of an Angular web app, I was satisfied this project is actually going to work. Justifying a move out from my collection of NodeJS tests and into its own repository. Another reason for moving into its own repository is that I wanted to make it easy to use angular-cli-ghpages to deploy my Angular app to GitHub pages. Where it will be served over HTTPS, a requirement for using sensor API from a web app.

Previously, I would execute such moves with a simple file copy, but that destroys my GitHub commit history. Not such a huge loss for small experiments like this one, but preserving history may be important in the future so I thought this is a good opportunity to practice. GitHub documentation has a page to address my scenario: Splitting a subfolder out into a new repository. It points us to a non-GitHub utility git-filter-repo which is a large Python script for manipulating git repositories in various ways, in this case isolating a particular directory and trimming the rest. I still had to manually move everything from a /compass/ subdirectory into the root, but that’s a minor change and git could recognize the files were renamed and not modified.

The move was seamless except for one detail: there is a conflict between local development and GitHub Pages deployment in its index.html. For GitHub Pages, I needed a line <base href="/compass/"> but for local development I needed <base href="/">. Otherwise the app fails to load because it is trying to load resources from the wrong paths resulting in HTTP404 Not Found errors. To make them consistent, I can tell my local development server to serve files under the compass subdirectory as well so I can use <base href="/compass/"> everywhere.

ng serve --serve-path compass

I don’t recall this being a problem in my “Tour of Heroes” tutorial. What did I miss? I believe using --serve-path is merely a workaround without understanding the root cause, but that’s good enough for today. It was more important that GitHub Pages is up and running and I could test across different browsers.


Source code for this project is publicly available on GitHub

Magnetometer Service as RxJS Practice

I’m still struggling to master CSS, but at least I got far enough to put everything I want on screen roughly where I want them, even after the user resizes their window. Spacing and proportion of my layout is still not ideal, but it’s good enough to proceed to the next step: piping data from W3C Generic Sensor API for Magnetometer into my Angular app. My initial experiment was a much simpler affair where I could freely use global variables and functions, but that approach would not scale to my future ambition to execute larger web app projects.

Installation

The Magnetometer API is exposed by the browser and not a code library, so I didn’t need to “npm install” any code. However, as it is not a part of core browser API, I do need to install W3C Generic Sensor API type definition for the TypeScript compiler. This information is only used at development time.

npm install --save-dev @types/w3c-generic-sensor

After this, TypeScript compiler still complains that it can’t find type information for Magnetometer. After a brief search I found I also need to edit tsconfig.app.json and add w3c-generic-sensor to “types” array under “compilerOptions”.

  "compilerOptions": {
    "types": [
      "w3c-generic-sensor",
    ]
  },

That handles what I had to do, but I’m ignorant as to why I had to do it. I didn’t have to do the same for @types/three when I installed Three.js, why was this different?

Practicing Both RxJS and Service Creation

I’ll need a mechanism to communicate magnetometer data when it is available. When the data is not available, I want to be able to differentiate between reasons why that data is not available. Either the software API is unable to connect to a hardware sensor, or the software API is not supported at all. The standard Angular architectural choice for such a role is to package it up as a service. Furthermore, magnetometer data is an ongoing stream of data, which makes it a perfect practice exercise for using RxJS in my magnetometer service. It will distribute magnetometer data as a Subject (multicast Observable) to all app components that subscribe to it.

Placeholder Data

Once I had it up and running, I realized everything is perfectly setup for me to generate placeholder data when real magnetometer data is not available. Client code for my magnetometer data service doesn’t have to change anything to receive placeholder data. In practice, this lets me test and polish the rest of my app without requiring that I run it on a phone with real magnetometer hardware.

State and Status Subjects

Happy with how this first use turned out, I converted more of my magnetometer service to use RxJS. The current state of the service (initialized, starting the API, connecting to magnetometer hardware, etc) was originally just a publicly accessible property, which is how I’ve always written such code in the past. But if any clients want to be notified as soon as the state changes, they either have to poll state or I have to write code to register & trigger callbacks which I rarely put in the effort to do. Now with RxJS in my toolbox, I can use a Behavior Subject to communicate changes in my service state, making it trivial to expose. And finally, I frequently send stuff to console.log() to communicate status messages, and I converted that to a Behavior Subject as well so I can put that data onscreen. This is extra valuable once my app is running on phone hardware, as I can’t (easily) see that debug console output.

RxJS Appreciation

After a very confused introduction to RxJS and a frustrating climb up the learning curve, I’m glad to finally see some payoff for my investment in study time. I’m not yet ready to call myself a fan of RxJS, but I feel I have enough confidence to wield this tool for solving problems. This story is to be continued!

With a successful Angular service distributing data via RxJS, I think this “rewrite magnetometer test in Angular” is actually going to happen. Good enough for it to move into its own code repository.


Source code for this project is publicly available on GitHub

Angular Component Dynamic Resizing

Learning to work within Angular framework, I had to figure out how to get my content onscreen at the location and size I wanted. (Close enough, at least.) But that’s just the start. What happens when the user resizes the window? That opens a separate can of worms. In my RGB332 color picker web app, the color picker is the only thing onscreen. This global scope meant I could listen to Window resize event, but listening to window-level event isn’t necessarily going to work for solving a local element-level problem.

So how does an Angular component listen to an event? I found several approaches.

It’s not clear what tradeoffs are involved using Renderer versus EventManager. In both cases, we can listen to events on an object that’s not necessarily our component. Perhaps some elements are valid for one API versus another? Perhaps there’s a prioritization I need to worry about? If I only care about listening to events that apply to my own specific component, things can be slightly easier:

  • @HostListener decorator allows us to attach a component method as the listener callback to an event on a component’s host object. It’s not as limited as it appears at first glance, as events like “window:resize” apparently propagates through the tree so our handler will fire even though it’s not on the Window object.

In all of the above cases, we’re listening on a global event (window.resize) to solve a local problem (react to my element’s change in size.) I was glad to see that web standards evolved to give us a local tool for solving this local problem:

  • ResizeObserver is not something supported by core Angular infrastructure. I could write the code to interact with it myself, but someone has written an Angular module for ResizeObserver. This is part of a larger “Web APIs for Angular” project with several other modules with similar goals: give an “Angular-y” way to leverage standardized APIs.

I tried this new shiny first, but my callback function didn’t fire when I resized the window. I’m not sure if the problem was the API, the Angular module, my usage of it, or that my scenario not lining up with the intended use case. With so many unknowns, I backed off for now. Maybe I’ll revisit this later.

Falling back to @HostListener, I could react to “window.resize” and that callback did fire when I resized the window. However, clientWidth/clientHeight size information is unreliable and my Three.js object is not the right size to fill its parent <DIV>. I deduced that when “window:resize” fired, we have yet to run through full page layout.

With that setback, I fell back to an even cruder method: upon every call to my animation frame callback, I check the <DIV> clientWidth/clientHeight and resize my Three.js renderer if it’s different from existing values. This feels inelegant but it’ll have to do until I have a better understanding of how ResizeObserver (or an alternative standardized local scope mechanism) works.

But that can wait, I have lots to learn with what I have on hand. Starting with RxJS and magnetometer.


Source code for this project is publicly available on GitHub

Angular Component Layout Sizing

For my first attempt at getting Three.js running inside an Angular web app, the target element’s width and height were hard coded to a number of pixels to make things easier. But if I want to make a more polished app, I need to make this element fit properly within an overall application layout. Layout is one of the things Angular defers to standard CSS. Translating layouts I have in my mind to CSS has been an ongoing challenge for me to master so this project will be another practice opportunity.

Right now, I’m using a <DIV> in my markup to host a canvas object generated by Three.js renderer. Once my Angular component has been laid out, I need to get the size of that <DIV> and communicate it to Three.js. A little experimentation with CSS-related properties indicated my <DIV> object’s clientWidth and clientHeight were best fit for the job.

Using clientWidth was straightforward, but clientHeight started out at zero. This is because during layout, the display engine looked at my <DIV> and saw it had no content. The canvas isn’t added until after initial layout in AfterViewInit hook of Angular component lifecycle. I have to create CSS to block out space for this <DIV> during layout despite lack of content at the time. My first effort was to declare a height using “vh” unit (Viewport Height) to stake my claim on a fraction of the screen, but that is not flexible for general layout. A better answer came later with Flexbox. By putting “display: flex;” on my <DIV> parent, and “flex-grow:1” on the <DIV> itself, I declared that this Three.js canvas should be given all available remaining space. That accomplished my goal and felt like a more generally applicable solution. A reference I found useful during this adventure was the Flexbox guide from CSS-Tricks.com.

It’s still not perfect, though. It is quite possible Flexbox was not the right tool for the job, but I needed this practice to learn a baseline from which I can compare with another tool such as CSS Grid. And of course, getting a layout up on screen is literally just the beginning: what happens when the user resizes their window? Dynamically reacting to resize is its own adventure.


Source code for this project is publicly available on GitHub

Angular + Three.js Hello World

I decided to rewrite my magnetometer test app as an Angular web app. Ideally, I would end up with something more polished. But that is a secondary goal. The primary goal is to use it as a practice exercise for building web apps with Angular. Because there are definitely some adjustments to make when I can’t just use global variables and functions for everything.

My first task is to learn how to use Three.js 3D rendering library from within an Angular web app. I know this is doable from others who have written up their experience, I only have to follow their lead.

Installation

The first step is obvious: install Three.js library itself into my project.

npm install --save three

Now my Angular app could import objects from Three, but it would fail TypeScript compilation because the compiler doesn’t have type information. For that, a separate library needs to be installed. This is only used at development time, so I save it as a development-only dependency.

npm install --save-dev @types/three

HTML DOM Access via @ViewChild

Once installed I could create an Angular component using Three.js. Inside that component I could use most of the code from Three.js introduction “Creating a Scene“. One line I could not use directly is:

document.body.appendChild( renderer.domElement );

Because now I can’t just jam something to the end of my HTML document’s <BODY> element. I need to stay within the bounds of my Angular component. To do so, I name an element in my component template HTML file where I want my Three.js canvas to reside.

[...Component template HTML...]

  <div #threejstarget></div>

[...Component template HTML...]

In the TypeScript code file, I can obtain a reference to this element with the @ViewChild decorator.

  @ViewChild('threejstarget') targetdiv!: ElementRef;

Why the “!” suffix? If we declared the variable “targetdiv” by itself, TypeScript compiler would complain that we risk using a variable that may be null or undefined instead of its declared type. This is because TypeScript compiler doesn’t know @ViewChild will handle that initialization. We use an exclamation mark (!) suffix to silence this specific check on this specific variable without having to turn on the (generally useful) null/undefined checks in TypeScript.

(On the “To-Do” list: come back later and better understand how @ViewChild relates to similar directives @ViewChildren, @ContentChild, and @ContentChildren.)

Wait until AfterViewInit

But there are limits to @ViewChild power. Our ElementRef still starts null when our component is initialized. @ViewChild could not give us a reference until the component template view has been created. So we have to wait until the AfterViewInit stage of Angular component lifecycle before adding Three.js render canvas into our component view tree.

  ngAfterViewInit() : void {
    this.targetdiv.nativeElement.appendChild( this.renderer.domElement );
  }

An alternative approach is to have <CANVAS> inside our component template, and attach our renderer to that canvas instead of appending a canvas created by renderer.domElement. I don’t yet understand the relevant tradeoffs between these two approaches.

Animation Callback and JavaScript ‘this

At this point I had a Three.js object onscreen, but it did not animate even though my requestAnimationFrame() callback function was being called. A bit of debugging pointed to my mistaken understanding of how JavaScript handles an object’s “this” reference. My animation callback was getting called in a context where it was missing a “this” reference back to my Angular component, and thus unable to advance the animation sequence.

requestAnimationFrame(this.renderNeedle);

One resolution to this issue (JavaScript is very flexible, there are many other ways) is to declare a callback that has an appropriate “this” reference saved within it for use.

requestAnimationFrame((timestamp)=>this.renderNeedle(timestamp));

That’s a fairly trivial problem and it was my own fault. There are lots more to learn about animating and Angular from others online, like this writeup about an animated counter.

Increase Size Budget

After that work I had a small Three.js animated object in my Angular application. When I run “ng build” at that point, I would see a warning:

Warning: bundle initial exceeded maximum budget. Budget 500.00 kB was not met by 167.17 kB with a total of 667.17 kB.

An empty Angular application already weighs in at over 200kB. Once we have Three.js in the deployment bundle, that figure ballooned by over 400kB and exceeded the default warning threshold of 500kB. This is a sizable increase, but thanks to optimization tools in the Angular build pipeline, this is actually far smaller than my simple test app. My test app itself may be tiny, but it downloaded the entire Three.js module from a CDN (content distribution network) and that file three.module.js is over a megabyte (~1171kB) in size. By that logic this is the better of two options, we just have to adjust the maximumWarning threshold in angular.json accordingly.

My first draft used a fixed-size <DIV> as my Three.js target, which is easy but wouldn’t be responsive to different browser situations. For that I need to learn how to use CSS layout for my target <DIV>


Source code for this project is publicly available on GitHub

Compass Web App for Angular Practice

I’ve wanted to learn web development for years and one of my problems was that I lacked the dedication and focus to build my skill before I got distracted by something else. This is a problem because web development world moves so fast that, by the time I returned, the landscape has drastically changed with something new and shiny to attract my attention. When I window-shopped Polymer/Lit, I was about to start the cycle again. But then I decided to back off for a few reasons.

First and most obvious is that I didn’t yet know enough to fully leverage the advantages of web components in general, and Polymer/Lit in particular. They enable small lightweight fast web apps but only if the developer knows how to create a build pipeline to actually make it happen. I have yet to learn how to build optimization stages like tree-shaking and minimization. Without these tools, my projects would end up downloading far larger files intended for developer readability (comments, meaningful variable names, etc.) and include components I don’t use. Doing so would defeat the intent of building something small lightweight.

That is closely related to the next factor: Angular framework has a ready setup of all of those things I have yet to master. Using Angular command line tools to build a new project boilerplate comes with a build pipeline that minimizes download size. I wasn’t terribly impressed by my first test run of this pipeline, but since I don’t yet know enough to setup my own, I definitely lack the skill to analyze why and certainly don’t yet know enough to do better.

And finally, I have already invested some time into learning Angular. There may be some “sunk cost fallacy” involved here but I’ve decided I should get basic proficiency with one framework just so I have a baseline to compare against other frameworks. If I redirect focus to Polymer/Lit, I really don’t know enough to understand its strengths and weaknesses against Angular or any other framework. How would I know if it lived up to “small lightweight fast” if I have nothing to compare it against?

Hence my next project is to redo my magnetometer web app using Angular framework. Such a simple app wouldn’t need all the power of Angular, but I wanted a simple practice run while things are still fresh in my mind. I thought about converting my AS7341 sensor web app into an Angular app, but those files must be hosted on an ESP32 which has limited space. (Part of the task would be converting to use ESPAsyncWebServer which supports GZip-compressed files.) In comparison, a magnetometer app would be hosted via GitHub pages (due to https:// requirement of sensor API) and would not have such a hard size constraint. Simpler deployment tipped the scales here, so I am going with a compass app starting with putting Three.js in an Angular boilerplate app.


Source code for this project is publicly available on GitHub