Developing Ionic application with Clojurescript - To-do App
1. Note
This blog post is a part of a series of blog posts about developing Clojurescript application using Ionic framework.
2. Before you begin
3. Issues with the generated project
When developing 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.
Bigger issue for me was bloat. The project generated by ionic start
has a lot of bloat. Typescript, Web components, a bunch of libraries and tools. None of these is required when developing in Clojurescript.
And worst of all, hot reload. While the way hot reload here works might be good for people used to Javascript development, it is way too inferior to what we got used when working with Clojurescript, since application was reloaded every time anything changes, including the whole application state. And it is not possible to tell ionic serve
to disable hot reload.
4. Create a project
But, by having a deeper look, Ionic is just a UI framework. There's nothing special about it, except that it provides a bunch of components typicaly used in web and mobile applications. It's the Capacitor that provides a runtime. And both can be added to a standard Javascript project, without using Ionic tools.
What about if we ditch ionic start
, just create a normal Clojurescript application and add Ionic afterwards? 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 re-frame-template project documentation.
5. Add Ionic
After verifying everything works, including the 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. Clojurescript compiler is good enough and it will not add anything that is not actually used to the relaese bundle.
To make a 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 in Clojars. Until then you can get it from here.
You also need to add required stylesheet to your resources/public/index.html
file in order to use Ionic. Unlike in Ionic which support Javascript import of Javafiles, these actually needs to be included in head section of 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 start 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 standard reagent
or re-frame
application, I will skipp that part. There are much better tutorials on the topic. I'll focus on introducing Ionic.
If you want to take a look at the re-frame specific part, please check iotodo.db
, iotodo.events
and iotodo.subs
namespaces.
UI code using Ionic will be located under iotodo.view
.
First, we'll create a todo screen and set it as 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 uses standard Ionic components for a basic page layout.
Save it, and the screen will appear in your browser automaticlly. If you encounter compilation errors due to undefined view components, you may define placeholders that will just print a component name instead. Something 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 todo 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 fully functionaly todo application example using Ionic and Clojurescript. For the reference complete view.cljs
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 an example source code on GitHub. Please check blog-post-1 tag for the source code that corresponds to this tutorial.
9. Introducing Webpack
Now that we have application that basicaly works, there are still some issues to address. When you open a console in the browser, there are some javascript errors. While we can ignore then for our simple todo-app example, it may bite us in the future when we start working on a bigger app.
Also, shadow-cljs build reports some warnings. It turns out, ionic libraries require a webpack Javascript packaging tool to run properly. Luckily, shadow-cljs is flexible enough that it can be configured to depend on third party tool to package required Javascript dependencies, and compile only Clojurescrip code using :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 witht the rest of dependencies.
First, let's configuere 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 external index file at the given location that includes all requires uded 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 content of ./js/index.js
and create bundle.js
file from everything includede 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 ionic framework as import.
Now we need to edit our index.html
and include generated bundle file before loading our app.js created by shadow-cljs. We can also remove importing 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 bundle we need to start the webpack
in a separate terminal ald 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 keep implementing the application. There's no more compile warnings or runtime errors, and we are good to go.
11. Source code
You can find a final example source code for this tutorial on GitHub. Please check blog-post-2 tag for the source code that corresponds to this tutorial at the time of writing.
12. Next steps
We may need to look into webpack options to enable some more optimizations. Webpack configuration file included here is basic, and probably can be further optimized.
Of course, deployment to mobile devices using Capacitor is yet to be tried. This may be comming in one of the next blog posts.