All Articles

Converting a server-rendered Python app into a single page Progressive Web App

I wrote before about a simple web app I created to teach my son maths. You can find the article here, the code here, and the live app here.

Looking at the code, you might wonder: why would you require server round-trip for every new card? Why can’t you just generate the next set of random dots on the client side?

I had the same thought, so I set about translating the code to client-side JavaScript. If you’re impatient, you can see the result here. If you want to know what I did, it was mostly these pretty painless steps:

  • Paste the existing code into a <SCRIPT> tag in an HTML file
  • Add parentheses around if conditions
  • Change the format of function definitions and if clauses, by using {} instead of :
  • Convert list comprehensions into for loops
  • Instead of assembling the DOM using string interpolation, do it piece by piece using document.createElementNS() and .setAttribute()
  • Replace some random.normalvariate(0,1) with Math.random() * 2 - 1, which doesn’t do the same thing, but works well enough
  • A very small amount of other cleanup

The above was enough to make the app work, but I was unhappy with needing to enter the min/max values in the URL. So I also used the excellent Tailwind CSS to add a footer and some buttons to increment/decrement these values. I wanted two-way binding between the values shown in the UI, and the values in the URL. Using a sophisticated mechanism (or a library) for such a simple app would be overkill, so I wrote some ugly code instead:

    function get_values_from_hash() {
      if (location.hash.length > 0) {
        [lower, upper] = location.hash.split("#")[1].split('/');
        lower = parseInt(lower);
        upper = parseInt(upper);
      } else {
        [lower, upper] = [1, 5]
      }
      return [lower, upper]
    }

    function set_values(lower, upper) {
      location.hash = "#" + lower.toString() + "/" + upper.toString();
      document.getElementById("lower").innerHTML = lower.toString();
      document.getElementById("upper").innerHTML = upper.toString();
    }

    function adjust_lower(n) {
      [lower, upper] = get_values_from_hash();
      lower = Math.max(1, lower + n);
      upper = Math.max(upper, lower + 4);
      set_values(lower, upper);
    }

    function adjust_upper(n) {
      [lower, upper] = get_values_from_hash();
      upper = Math.max(5, upper + n);
      lower = Math.min(lower, upper - 4);
      set_values(lower, upper);
    }

Now the code doesn’t need a server, I figured I should make it work offline, and installable as a PWA. It turned out this was pretty simple, using some example service worker code from Google, and adding a simple web manifest and icon.

There’s one thing left to do: make the bottom bar stick to the bottom of the screen. Currently, I’m using the CSS style height: 100vh via Tailwind’s h-screen utility class. This works fine on desktop, and when the app is installed and launched via a mobile launcher icon.

But, when the app is opened on a mobile web browser (e.g. Chrome on Android), the visible part of the page is shorter than the screen (due to the address bar and browser navigation bar), so part/all of my bottom bar scrolls off the page. This is a well known problem with using height: 100vh and there are lots of workarounds. I had a quick look, and none of the workarounds I saw look elegant, so I’ll fix this another day.

The app is at https://dots.twilam.com/. To see the source, just view source in your browser.