Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save jamesnyika/ab58fe4d75481d5119312d050664c681 to your computer and use it in GitHub Desktop.
Save jamesnyika/ab58fe4d75481d5119312d050664c681 to your computer and use it in GitHub Desktop.
Expo, NativeBase and Clojurescript (using Re-frame)

Expo and Native Base are fantastic environments for designing mobile applications. However, there are still many challenges that you will face in attempting to build an application with them. Just to make it a little harder, try doing it in Clojurescript where you have to translate what you would ordinarily do in Javascript..in Clojurescript instead.

In this short gist, I wanted to highlight some things you are going to see often. Very often and how to get around them.

Before we begin

  • You will notice a lot of namespaces names ui or uic or mod. I have separated in different files my library imports (mod namespace) from the actual component configuration (ui namespace) from the compositing of multiple components (uic namespace). So be mindful of that and do not just copy blindly. in most cases you if you want to use the code as is, all you need to do is strip out my namespaces before making calls.

  • I have only been testing with the IOS simulator. I have no confirmation that any of this so far is working on Android. Not because it does not, but because I have not yet gotten there and tested it out.

  • The is virtually NO documentation on a lot of how to do this with Clojurescript and it is purely learned through trial and error which is a terrible way to build an application. If you have a better way to do any of this - please please please let me know and save me/us some cycles.

0- Setting up Expo and Native Base in the same Clojurescript project.

I would first explain some benefits of doing this. While Expo does expose some superb components, it is not complete and there are some components such as Toasts that would be nice to have at your fingertips. How can we use them all ? First, you need to make sure you set up the project using a lein template that does most of that for you. Sean Tempesta has a great leiningen template for this here. Run this once and you get a very nice project with expo pretty much included in it. His basic steps are (copied from the site):

  1. Create your project
lein new expo your-project +reagent
lein new expo your-project +om
lein new expo your-project +rum

  1. Change into your project's directory
cd your-project

  1. Install npm dependencies
yarn install

  1. Start figwheel To auto-compile Clojurescript code and provide a development REPL
lein figwheel

  1. Start XDE and open the project's directory From here you can Publish, Share, or run the app on a device. See Expo's documentation for more info.

  2. [optional] Set lan-ip option via file: Create file named .lan-ip with your ip. This ip will be used by figwheel to connect via websockets. If this file is not present it gets the ip from the system.

In linux you can execute the following line to create the file.

source lan-ip.sh

1 - Exception: Unable to resolve module ... index.js. What do I do ?

This is glorious error and appears in bright red in your simulator. Confusing as ever but the solution is quite simple. You have an exception compiling in clojurescript. Your target .js file was never generated - probably due to an exception in your clojurescript code resulting in this exception. Fix your clojurescript, re-run lein and reload your simulator and you should be past it.

2 - How do you get Toasts, Alerts, Prompts and other such components to work ?

First the shocker. There are no Toasts exposed in Expo. But they are pretty damned useful to use. So here is how you get them and other components like them, to work.

  1. Make sure you have loaded up NativeBase. To include native base in expo you need to run yarn first

$ yarn add native-base

This will include Native Base in your expo app. Now you need to use it in your clojurescript app

(def NativeBase (js/require "native-base"))

We have now loaded it up. Want to see it ? in your repl (say after running lein figwheel you can type this to see what you get.

(js->clj NativeBase)

This will print something like this :

main:cljs.user=>  (def NativeBase (js/require "native-base"))
#'cljs.user/NativeBase
main:cljs.user=> (js->clj NativeBase)
{"Switch" #object[StyledComponent],
 "getTheme" #object[Function],
 "View" #object[StyledComponent],
 "FooterTab" #object[StyledComponent],
 "Left" #object[StyledComponent],
 "Picker" #object[StyledComponent],
 "Col" #object[ColumnNB],
 "InputGroup" #object[StyledComponent],
 "Button" #object[StyledComponent],
 "H3" #object[StyledComponent],
 "Header" #object[StyledComponent],
 "Radio" #object[StyledComponent],
 "Body" #object[StyledComponent],
 "Title" #object[StyledComponent],
 "Root" #object[StyledComponent],
 "StyleProvider" #object[StyleProvider],
 "Tab" #object[StyledComponent],
 "TabContainer" #object[StyledComponent],
 "Grid" #object[GridNB],
 "CardItem" #object[StyledComponent],
... (more here truncated)

These objects can then be 'adapted' for use within re-frame/re-natal applications as follows

First you need to have required reagent

 (:require  [reagent.core :as r :refer [atom]] 

Then you can adapt items like so

(def button (r/adapt-react-class (.-Button mod/NativeBase))) 

or you can use this style

(def button (r/adapt-react-class (aget mod/NativeBase "Button")))

(there are differences in these two ways - that is discussed in this article by Konrad Garus.

HOWEVER, Toasts, Alerts, Prompts ..and probably a few other components are different They have to be ACTIVATED or TRIGGERED to be shown.

For those components we use a slightly different style Here are the examples

;; define a function that can trigger an alert. Notice the conversion of datastructures to make them more JS friendly
(defn alert
  ([title]
   (.alert (aget mod/ReactNative "Alert") title))
  ([title message buttons]
   (.alert (aget mod/ReactNative "Alert") title message (clj->js buttons))))

;; prompts are treated similarly to alerts
(defn prompt
  [title message cb-or-buttons]
  (if (ios?)
    (.prompt (aget mod/ReactNative"AlertIOS") title message cb-or-buttons)))

;;This works too in a similar way. However, there is no toast function. Toasts are triggered with a show function.
(defn toastmsg [message]
  (.show (aget mod/NativeBase "Toast") (clj->js {:buttonText "Ok" :text message :position "bottom" :type "danger" :duration 5000})))

This is not entirely my code - I got this from the great Tienson Qin That is a fantastic source of info on how to do the Expo/Clojurescript integration.

So notice the difference

  • we access the component from the library using the aget syntax.
  • we then invoke the appropriate trigger function and pass it data either as a datastructure or as raw arguments
  • now that you have a function to trigger - you can invoke it anonymously from a button component or even from within a re-frame handler function.

3 - What about functions that require permissions to be obtained from the user, such as the camera or notifications ?

Glad you asked that. If you look closely at the Expo components, one of them is a Permissions object. This is the key. However, this object works asynchronously and requires use of promises. So here is the supporting code (again from Tienson Qin ). I have made modifications to make it a generic permission seeking function


(defn getPermissionsForUse
  [permission]
  ;; Android remote notification permissions are granted during the app
  ;; install, so this will only ask on iOS
  (go
    (let [status (-> (.askAsync mod/Permissions (aget mod/Permissions permission))
                     (util/promise->chan)
                     (async/<!)
                     (aget "status"))]

        ;;post this permission
        (dispatch [:register-permission {:permission permission :status status}])
     )))

Let me unpack this :

  • First, you need to run this inside a go loop so you need core.async installed and set up in the require. Here is mine - keep in mind, you do not see my namespace information that comes above this
  (:require-macros [cljs.core.async.macros :refer [go]])
  (:require [cljs.core.async :as async])
  • Next, we start a go loop and open a let context. Inside here we obtain the status of the permission in question. This permission is passed in as a string argument like "camera" or "location".

  • the util/promise->chan function is a way of extracting promise values (with the status) into a channel. Here is that function

;;you need to include the promise-chan function in core async
(:require-macros [cljs.core.async.macros :refer [go]])
(:require [cljs.core.async :refer [put! promise-chan]]

(defn promise->chan
  ([promise]
   ;;(.log js/console "promise->chan with one argument  " promise)
   (promise->chan promise nil))
  ([promise error-handler]
   (let [c (promise-chan)]
     ;;(.log js/console "promise->chan with 2 arguments " promise)
     (-> promise
         (.then (fn [result] (put! c result)))
         (.catch (fn [error] (error-handler error))))
     c)))
     

This function relies on the core.async promise-chan function to work. So if it fails, check your core.async version to ensure you are up to date.

*then we extract the promised value from the channel and pull out the status variable.

So you now have a function that you can trigger from a button if you want to turn on a barcode reader (requires Camera permissions) or GPS tracking (location permission) or audio recording etc.

4 - I have a form. But when I extract values from the fields they are wrapped in a strange object. How do I get my values out ?

This was a bit of a shocker to me to. I thought I would get values directly as you do on a web page but not with react native. I instead got this function (from another source) to get those out

;;----------VALUE EXTRACTOR FOR NATIVE FIELDS----------
(defn extract [value]
  (-> value
      .-nativeEvent
      .-text)
  )

and then i use it like so

;; example of use to extract value from a field (even password field) 
;; data here is an reagent atom (ratom) that will be populated with any data typed into the component 

[ui/input {:secureTextEntry secure?  :on-change #(swap! data assoc datakey (extract %)) :value (@data datakey) } ]

Works very well.

5 - How about how to configure and set up a few other components ? any examples ?

Sure thing. First thing I did I wrapped my outemost container with a Root component - some form of container. Most of these NativeBase components require this. I am still not sure what this does but as you can see, the documentation requires it.

(def root (r/adapt-react-class (.-Root mod/NativeBase))) 
;;then wrap outer most component 

;;this is from my core.cljs file. My app-root method returns my current screen rapped in the root component from my ui namespace.
(defn app-root []
  (let [current-screen (subscribe [:screen/current-screen])]
    (fn []
      ;;we begin by displaying the splash screen for 3 secs and then show the login prompt
      ;;display splash
      [ui/root @current-screen]
      )))
      
      

The ActionSheet

(defn actionsheet []

  ;; Actionsheet show call requires two params: options and a call back funtion
  ;; PRO-TIP: You should modify this function to include parameters for all the stuff I have hardcoded here
  (.show (aget mod/NativeBase "ActionSheet") 
         (clj->js
         {:options ["Option 1" "Option 2" "Cancel" "Delete"] :cancelButtonIndex 2 :destructiveButtonIndex 3 :title "New ActionSheet"
          :message "Action Message" :tintColor "teal"}) 
          #(alert %)))
          
  ;;and you can now trigger it -say with a button. 
  [button {:on-press #(actionsheet)}]

AlertIOS Prompt

  • Source - NativeBase
  • Key Insight: Requires a trigger function
  • Wrapped Component : React Native AlertIOS
  • Code
(defn alert
  ([title]
   (.alert (aget mod/ReactNative "Alert") title))
  ([title message buttons]
   (.alert (aget mod/ReactNative "Alert") title message (clj->js buttons))))

(defn prompt
  [title message cb-or-buttons]
  (if (ios?)
    (.prompt (aget mod/ReactNative"AlertIOS") title message cb-or-buttons)))

   ;;and you can now trigger it -say with a button. 
  [button {:on-press #(alert "This gist needs to be favorited")}]

Calendars

yarn add react-native-calendars
  • Wrapped Component : unknown or custom
  • Code
;first import the react-native calendars components
;;This is the core import for the react native calendar component. I did this in a namespace i later call mod/
(def ReactNativeCalendars (js/require "react-native-calendars"))

;;-------------react native calendars ---------------------
;;begin here: import native calendars which you installed. mod refers to the namespace in the line above which i have in a different ;;file
(def calendar (r/adapt-react-class (.-Calendar mod/ReactNativeCalendars)))
(def calendarlist (r/adapt-react-class (.-CalendarList mod/ReactNativeCalendars)))
(def agenda (r/adapt-react-class (.-Agenda mod/ReactNativeCalendars)))

;;now use on your screens. The agenda view is particularly beautiful
 ;;display the calendar
    [ui/agenda]

Notice that I have not yet put in any data into the calendar but it should at least show up. You can then read up on how to add data to display. I might add more of that in another gist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment