Developing Ionic application with Clojurescript - To-do App
1. Note
This blog post is part of a series about developing Clojurescript applications using the Ionic framework.
2. Before you begin
3. Issues with the generated project
When developing the first part, I noticed some issues. For example, the code was too verbose; every time I had to write [:> ion/IonButton ...]
instead of just [ion/button ...]
. That kind of verbosity can be overcome by having a wrapper over the Ionic UI components.
A bigger issue for me was bloat. The project generated by ionic start
has a lot of bloat. TypeScript, web components, and a bunch of libraries and tools – none of these are required when developing in Clojurescript.
And worst of all, hot reload. While the way hot reload works here might be good for people used to JavaScript development, it is far inferior to what we are used to in Clojurescript, since the application is reloaded every time something changes, including the whole application state. And it is not possible to instruct ionic serve
to disable hot reload.
4. Create a project
But, upon closer inspection, Ionic is just a UI framework. There's nothing special about it except that it provides a bunch of components typically used in web and mobile applications. It’s Capacitor that provides a runtime. Both can be added to a standard JavaScript project without using Ionic tools.
What if we ditch ionic start
and simply create a normal Clojurescript application and add Ionic afterward? Let's just do it.
We will use re-frame-template to create a standard re-frame
project with a pair of goodies built in.
lein new re-frame iotodo +cider +10x +re-frisk
cd iotodo
For a full list of options and how to use it, please check the re-frame-template project documentation.
5. Add Ionic
After verifying that everything works, including hot reload, let's add Ionic and some related packages.
npm install @ionic/react npm install @ionic/react-router npm install ionicons npm install @capacitor/app npm install @capacitor/core npm install @capacitor/haptics npm install @capacitor/keyboard npm install @capacitor/status-bar
I added a bunch of packages that may not be required, but I don't care. The Clojurescript compiler is good enough and will not add anything that is not actually used to the release bundle.
To make working with Ionic more pleasant, I created an ion.core
package that wraps all the Ionic components into Reagent components. Something like this:
(ns ion.core (:refer-clojure :exclude [range list]) (:require [reagent.core :as r] ["@ionic/react" :as i])) (defn- adapt-class [class] (when class (r/adapt-react-class class))) ;; Action Sheet (def action-sheet (adapt-class i/IonActionSheet)) ;; Accordion (def accordion (adapt-class i/IonAccordion)) (def accordion-group (adapt-class i/IonAccordionGroup)) ...
If I don't get lazy, I may publish this as a library on Clojars. Until then, you can get it from here.
You also need to add the required stylesheet to your resources/public/index.html
file to use Ionic. Unlike Ionic—which supports JavaScript imports of CSS files—these stylesheets need to be included in the head section of the index file.
Full file included as a reference.
<!DOCTYPE html> <html lang="en"> <head> <meta charset='utf-8'> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>iotodo</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/react@6.0.8/css/ionic.bundle.css" /> </head> <body> <noscript> iotodo is a JavaScript app. Please enable JavaScript to continue. </noscript> <div id="app"></div> <script src="/js/compiled/app.js"></script> </body> </html>
Let's start the application and begin developing.
npm run watch # or npx shadow-cljs watch app
6. Data model
Since the purpose of this tutorial is not to show how to develop a standard reagent
or re-frame
application, I will skip that part. There are many better tutorials on this topic. I'll focus on introducing Ionic.
If you want to take a look at the re-frame-specific part, please check the iotodo.db
, iotodo.events
and iotodo.subs
namespaces.
UI code using Ionic will be located in iotodo.view
.
First, we'll create a todo screen and set it as the application main-panel
(defn todo-screen [] [ion/app [ion/header [ion/toolbar [ion/title "Todo List"]]] [ion/content {:class "IosPadding"} [ion/grid [ion/row [input-container]] [ion/row [todos]]]]]) (defn main-panel [] [todo-screen])
This example uses standard Ionic components for a basic page layout.
Save the file, and the screen will appear in your browser automatically. If you encounter compilation errors due to undefined view components, you can define placeholders that will print the component name instead, like this:
(defn input-container [] "input-container")
Once you have a basic application layout, you can implement a list of items:
(defn todo-item [{:keys [key text checked]}] [ion/item {:key key} [ion/checkbox {:slot "start" :checked checked :on-ion-change #(rf/dispatch [:todos/check key])}] [ion/text {:style {:text-decoration (if checked :line-through :none)}} text] [ion/button {:slot "end" :icon-only true :on-click #(rf/dispatch [:todos/delete key])} [ion/icon {:icon icons/trash}]]]) (defn todos [] (let [todos (rf/subscribe [:todos/all])] [ion/list (for [todo @todos] [todo-item todo])]))
7. Input component
Now that the list management is implemented, the only thing left is the implementation of the component that will create a new to-do item.
(defn- input-container [] (let [value (r/atom "")] (fn [] [ion/item [ion/textarea {:slot :start :placeholder "Todo ..." :auto-grow true :autofocus true :inputmode "text" :value @value :on-ion-change #(reset! value (.. % -detail -value))}] [ion/button {:slot "end" :icon-only true :on-click (fn [] (rf/dispatch [:todos/add @value]) (reset! value ""))} [ion/icon {:icon icons/add}]]])))
Now we have a fully functional to-do application example using Ionic and Clojurescript. For reference, the complete `view.cljs` file looks like this:
(ns iotodo.views (:require [reagent.core :as r] [re-frame.core :as rf] [ion.core :as ion] ["ionicons/icons" :as icons] [iotodo.subs :as subs])) (defn- input-container [] (let [value (r/atom "")] (fn [] [ion/item [ion/textarea {:slot :start :placeholder "Todo ..." :auto-grow true :autofocus true :inputmode "text" :value @value :on-ion-change #(reset! value (.. % -detail -value))}] [ion/button {:slot "end" :icon-only true :on-click (fn [] (rf/dispatch [:todos/add @value]) (reset! value ""))} [ion/icon {:icon icons/add}]]]))) (defn- todo-item [{:keys [key text checked]}] [ion/item {:key key} [ion/checkbox {:slot "start" :checked checked :on-ion-change #(rf/dispatch [:todos/check key])}] [ion/text {:style {:text-decoration (if checked :line-through :none)}} text] [ion/button {:slot "end" :icon-only true :on-click #(rf/dispatch [:todos/delete key])} [ion/icon {:icon icons/trash}]]]) (defn- todos [] (let [todos (rf/subscribe [:todos/all])] [ion/list [input-container] (for [todo @todos] [todo-item todo])])) (defn- todo-screen [] [ion/app [ion/header [ion/toolbar [ion/title "Todo List"]]] [ion/content {:class "IosPadding"} [ion/grid [ion/row [todos]]]]]) (defn main-panel [] [todo-screen])
8. Source code
You can find the example source code on GitHub. Please check the blog-post-1 tag for the source code that corresponds to this tutorial.
9. Introducing Webpack
Now that we have an application that basically works, there are still some issues to address. When you open the console in the browser, there are some JavaScript errors. While we can ignore them for our simple to-do app example, it might bite us in the future when we start working on a bigger app.
Also, the shadow-cljs build reports some warnings. It turns out, Ionic libraries require the webpack JavaScript packaging tool to run properly. Luckily, shadow-cljs is flexible enough that it can be configured to depend on a third-party tool to package required JavaScript dependencies, and compile only ClojureScript code using the :js-options
configuration.
First, let's add webpack
to the project.
npm install --save-dev webpack npm install --save-dev css-loader npm install --save-dev style-loader
We also added css-loader
and style-loader
modules since we'll need them for bundling ionic
CSS files together with the rest of the dependencies.
First, let's configure our `shadow-cljs.edn` as explained above:
:builds {:app {:target :browser :output-dir "resources/public/js/compiled" :asset-path "/js/compiled" :js-options {:js-provider :external :external-index "js/target/requires.js"} ...
This will tell shadow-cljs to generate an external index file at the given location that includes all requires used in our ClojureScript application.
Now we need to configure Webpack. Create the webpack.config.js
file with the following configuration:
const path = require('path'); module.exports = { mode: "production", entry: ['./js/index.js'], output: { path: path.resolve(__dirname, 'resources/public/js/libs'), filename: 'bundle.js', clean: true, }, module: { rules: [ { test: /\.m?js/, resolve: { fullySpecified: false, } }, { test: /\.css$/, use: ['style-loader', 'css-loader'] } ] }, };
This will tell webpack to get the content of ./js/index.js
and create a bundle.js
file from everything included from there. We need to create that ./js/index.js
like this:
import './target/requires.js'; /* Core CSS required for Ionic components to work properly */ import '@ionic/react/css/core.css'; /* Basic CSS for apps built with Ionic */ import '@ionic/react/css/normalize.css'; import '@ionic/react/css/structure.css'; import '@ionic/react/css/typography.css'; /* Optional CSS utils that can be commented out */ import '@ionic/react/css/padding.css'; import '@ionic/react/css/float-elements.css'; import '@ionic/react/css/text-alignment.css'; import '@ionic/react/css/text-transformation.css'; import '@ionic/react/css/flex-utils.css'; import '@ionic/react/css/display.css';
This will import files with our requires generated by shadow-cljs
as well as add CSS files required by the Ionic framework as imports.
Now we need to edit our index.html
and include the generated bundle file before loading our app.js created by shadow-cljs
. We can also remove the import of the CSS file, since it's now included in a library bundled by webpack.
<!DOCTYPE html> <html lang="en"> <head> <meta charset='utf-8'> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>iotodo</title> </head> <body> <noscript> iotodo is a JavaScript app. Please enable JavaScript to continue. </noscript> <div id="app"></div> <script src="/js/libs/bundle.js"></script> <script src="/js/compiled/app.js"></script> </body> </html>
10. Compiling and starting the application
In order to create the bundle, we need to start the webpack
in a separate terminal and let it watch for changes.
npx webpack --watch
We do the same with shadow-cljs:
npx shadow-cljs watch app
Now we can open http://localhost:8280 and continue implementing the application. There are no more compilation warnings or runtime errors, and we are good to go.
11. Source code
You can find the final example source code for this tutorial on GitHub. Please check the blog-post-2 tag for the source code corresponding to this tutorial at the time of writing.
12. Next steps
We might need to look into webpack options to enable additional optimizations. The included webpack configuration file is basic and could probably be further optimized.
Of course, deployment to mobile devices using Capacitor has yet to be attempted. This may be coming in one of the next blog posts.