jmpavlick / elm-todomvc-capacitor

The iconic elm-todomvc project, as an Ionic Capacitor mobile application

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

TodoMVC in Elm, with Ionic Capacitor!

This repo is a minimal port of the classic evancz/elm-todomvc application, with the additional infrastructure necessary to run as a native, cross-platform mobile application powered by Ionic Capacitor.

All of the application code is in Elm! To get started quickly, simply fork this repo, then scroll on down to the Caveats section for a brief overview of the biggest difference between Elm applications on the web, and Elm applications in Ionic Capacitor.

But wait - there's more! This repo serves as a tutorial. I've tried to build the different layers of this application as a set of pull requests that are all linked in this document. The comments on the pull request show which changes were made, when, and why - and you can read an overview for each major changeset here in this readme, as well.

If you have questions, feel free to shoot me a DM in Elm Slack, or ping me on Twitter - @lambdapriest.

Step 1 - Start with an existing Elm application (or make a new one!)

Pull Request

For this demo application, I've started with an existing app; but you can do whatever you like. There are some limitations, though; you'll have to create a Browser.element or Browser.document application. (For more notes on this, see Caveats - URL Routing / Navigation.)

This repo starts with a simple Elm app that only contains what's absolutely necessary - an elm.json file, a src/Main.elm file, and an index.html file to host the built JavaScript. In fact, if you check out the project and navigate to the Git hash that was merged in during the Step 1 PR, you can build the app just by navigating to the root of the project, and running:

elm make src/Main.elm --output=elm.js

Then opening index.html in a web browser.

Step 2 - npm and a bundler

Pull Request

We'll need npm in order to install the dependencies we need to get going with Capacitor; and if we get set up with a bundler now, we can have nice things like build-on-save while we're working.

In this step, we installed the following npm packages:

  • vite
  • vite-plugin-elm

Then, we refactored the little bit of JavaScript in index.html that hosts our elm.js file into a module called index.mjs, which will make it easier to integrate with our bundler, Vite, as well as with some of the other Capacitor packages we'll need.

We added vite-plugin-elm, because it lets you import your src/Main.elm file directly into your index.mjs file, which Vite automatically picks up on build. We configured the vite.config.js file to run vite-plugin-elm during our build step.

Finally, we added a simple npm script, start. It sets the ENV environment variable to DEV and runs vite --host. Our vite.config.js uses the ENV value to set a JSON object that it passes to the elmPlugin function from vite-plugin-elm, which sets the --debug and --optimize flags from elm make. (We want to se the debugger when we're in our development environment, but we don't want to see it when we do a release build.)

Step 3 - Install Capacitor and make it runnable on Android and iOS

Pull Request

Note: The only files that were hand-edited in this pull request are package.json, Brewfile, and .gitignore. The rest of the assets on this PR are auto-generated by the commands that we will run in this section, which makes the diff not very nice to read. Still, it's instructive to see what Capacitor is doing for us, so don't be afraid to poke around a little!

Now that we have npm and a bundler, we can add Capacitor to our project. This is remarkably simple - the docs are here, but I'll summarize:

Install Capacitor

  • npm install @capacitor/core
  • npm install -D @capacitor/cli

Initialize Capacitor

  • npx cap init, and follow the prompts. This creates capacitor.config.json.

Install the Android and iOS platform plugins

  • npm install @capacitor/android @capacitor/ios

Create the Android and iOS projects

  • npx cap add android
  • npx cap add ios

This will create Android and / or iOS projects (depending on what platform plugins you have installed, and which of these commands you run), at android/ and ios/ respectively.

The iOS project has a dependency on CocoaPods, so you'll need to install that. My preferred method is to install it via Homebrew. I've added a Brewfile to this project; if you have Homebrew installed already, simply run

brew bundle

from the root directory, before you run npx cap add ios.

Sync

Here's where we diverge slightly from the docs.

If you run npx cap sync, Capacitor will copy whatever's in the folder specified in the webDir value of capacitor.config.json - for us, that's "dist" - into those projects.

Right now, our dist folder contains whatever Vite bundled the last time it ran - if it even ran at all (i.e., if your project is building in a CI pipeline, or if a friend is helping you and they just pulled this repo down on their computer). When we finish our app and want to release it to the App Store, we'll need to make sure that our built output doesn't include the Elm debugger; so it's helpful to wrap the call to npx cap sync in another npm script so that we can specify the environment that our Elm app is building in.

So to enable this, we've added two more npm scripts:

  • sync-dev: Runs the Vite build with ENV=DEV, then runs npx cap sync
  • sync: Same as sync-dev, but without ENV=DEV so that our built output is optimized and has the Elm debugger removed

Opening Your Project

You can run npx cap open android to open your Android project in Android Studio; you can run npx cap open ios to open your iOS project in Xcode.

Because I hate remembering commands, I've added these as npm scripts - open-android and open-ios, respectively. Adding them to my package.json ensures that all of the commands that I need to run in order to work on this application are written down somewhere as part of my project.

Note: Capacitor doesn't actually build binaries of your mobile apps for their respective platforms; it just maintains a project for each platform, and copies your web artifacts over when you run npx cap sync. If you're using Windows or Linux, you'll still have to have a Mac to build and run an iOS project; fortunately, cloud CI solutions exist!

Now that your project is open, you can build and run it in the simulator of your choice! You'll notice that the app isn't drawing correctly in the viewport - depending on the simulator you run with, perhaps a "notch" is covering part of the content; perhaps the content is sized poorly or off-center; and when you tap a text input to enter a "todo" item, the display may zoom in on the field while the keyboard opens.

All of these are very un-app-y behaviors; the bulk of the work that we have left to do concerns making it so that our web application behaves like a native mobile application.

Running Your Project And Looking At Changes

If you've been making changes to your project and you want to see how it looks in an emulator on your device - run npm sync-dev or npm sync, then rebuild and restart the emulator.

A Note On Source Control

On one hand, the android/ and ios/ folders are a sort of build artifact, in that they are auto-generated by npx cap sync; but on the other hand, you can edit some of the config files in those directories, and those changes will be persisted across repeated runs of npx cap sync - so in practice, for me, it's much easier to have them included in source control.

Step 4 - Capacitor Platform Integration

Pull Request

At this point, we have our Elm application running as a native mobile app, but it doesn't feel very "app"-y.

To fix this, we're going to wire in some of Capacitor's platform hooks, so that we can give our Elm application more information about its runtime environment - as well as make some small layout tweaks in order to get it to behave.

In order to access those hooks, we'll need to:

  • npm install @capacitor/device - docs
  • npm install capacitor-plugin-safe-area - docs

The two values we care most about are:

  • Is this app running on a device, or an an emulator (i.e., is it virtual or not)?
    • Believe it or not, we really care about this - if, for instance, you want to show ads in your app? You had better make sure that your app doesn't show ads during testing, because Google will flag your AdMob account for "generating fraudulent ad traffic" when it runs your app in hundreds of testing VMs to make sure that it boots on every possible version of Android, ever; and that is not fun (ask me how I know).
  • What's the offset for the "safe area" at the top of the screen? How much padding do we need so that we don't accidentally draw behind a notch or camera hole?

You'll have to update index.mjs to call the functions from @capacitor/device and capacitor-plugin-safe-area in order to pull back device info and send it in as part of your flags value; and update src/Main.elm to accept the updated flags value, as well as operate on the new values you're sending in.

Finally, we need to add one line of code to index.html to set our Capacitor app's browser to constrain the content to the width of the device, and to disable zooming.

(The pull request dives in a little deeper here with the code changes; it's recommended reading!)

Step 5 - Icons and Splashscreen

Pull Request

In order to release your application on the App Store and Google Play Store, you're going to need app icons and a splashscreen. Since we're letting Capacitor control virtually all of the particularities about building our apps, we'll also need to use Capacitor tooling to generate our app icons and splashscreen and build them into our Xcode and Android Studio projects.

Fortunately, cordova-res makes this easy. Just run:

  • npm install cordova-res docs

I've added an npm script, icons-splashscreen, to call it with the appropriate arguments and generate the resources, and I've added the call to npm run icons-splashscreen in to the sync and sync-dev scripts, for convenience's sake.

Step 6 - The App Store / Play Store, And Beyond!

Just kidding! Once you've got your app "done", you'll have to run the gauntlet to get your app uploaded, submitted, approved, and so on and so forth.

Fortunately, once you can build your app with Xcode and Android Studio - the rest of the process is pretty straightforward, and just like releasing any other app. There's more than enough written on the Internet about that, so I'll leave this part as an exercise for the reader.

Of course, there are other Capacitor plugins that you can use to access native device functionality - and since Capacitor is based on Apache Cordova, there are many other plugins that you may be able to use.

Congrats! If you've made it this far and you still have questions - again, you can find me in Elm Slack as @jmpavlick, or on Twitter @lambdapriest.

If you've found any of this to be in error, PRs are always welcome.

Caveats

URL Routing / Navigation

Due to limitations with Elm's URL routing and due to constraints on web views in iOS, you can't give your Elm application full control of the DOM, so you can't create your app as a Browser.application. The biggest limitation here is that you can't use elm/url routes or browser navigation - but that's really not a big deal!

URL routing only really matters when the URL bar should be available to the user as a means of input. Since the URL bar in the web viewer is hidden for an Ionic Capacitor app, you don't need to worry about it at all! You can simply store a value on your Model that tracks your user's location in the app, and you can retrieve that value from local storage when your app loads, through your flags.

For this reason, I strongly recommend using a Browser.element - so that you can use flags to send input parameters to your application, and so that you can use ports to set and retrieve values from the web viewer's local storage (which is persisted on the mobile filesystem). You can also use a Browser.document, but since your users will never see the <title> of the page, there's not really any reason to.

About

The iconic elm-todomvc project, as an Ionic Capacitor mobile application

License:BSD 3-Clause "New" or "Revised" License


Languages

Language:Elm 46.0%Language:CSS 28.0%Language:Swift 11.7%Language:Java 5.1%Language:JavaScript 4.9%Language:Ruby 3.1%Language:HTML 1.2%