├── .buckconfig ├── .eslintrc.js ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .prettierrc.js ├── .watchmanconfig ├── README.md ├── app.json ├── babel.config.js ├── deps.edn ├── index.js ├── metro.config.js ├── package.json ├── shadow-cljs.edn ├── src └── clojurernproject │ ├── core.cljs │ ├── events.cljs │ ├── subs.cljs │ └── views.cljs ├── test └── events │ └── counter_test.cljs └── yarn.lock /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | }; 5 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore polyfills 9 | node_modules/react-native/Libraries/polyfills/.* 10 | 11 | ; These should not be required directly 12 | ; require from fbjs/lib instead: require('fbjs/lib/warning') 13 | node_modules/warning/.* 14 | 15 | ; Flow doesn't support platforms 16 | .*/Libraries/Utilities/LoadingView.js 17 | 18 | [untyped] 19 | .*/node_modules/@react-native-community/cli/.*/.* 20 | 21 | [include] 22 | 23 | [libs] 24 | node_modules/react-native/Libraries/react-native/react-native-interface.js 25 | node_modules/react-native/flow/ 26 | 27 | [options] 28 | emoji=true 29 | 30 | esproposal.optional_chaining=enable 31 | esproposal.nullish_coalescing=enable 32 | 33 | module.file_ext=.js 34 | module.file_ext=.json 35 | module.file_ext=.ios.js 36 | 37 | munge_underscores=true 38 | 39 | module.name_mapper='^react-native$' -> '/node_modules/react-native/Libraries/react-native/react-native-implementation' 40 | module.name_mapper='^react-native/\(.*\)$' -> '/node_modules/react-native/\1' 41 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' 42 | 43 | suppress_type=$FlowIssue 44 | suppress_type=$FlowFixMe 45 | suppress_type=$FlowFixMeProps 46 | suppress_type=$FlowFixMeState 47 | 48 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) 49 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ 50 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 51 | 52 | [lints] 53 | sketchy-null-number=warn 54 | sketchy-null-mixed=warn 55 | sketchy-number=warn 56 | untyped-type-import=warn 57 | nonstrict-import=warn 58 | deprecated-type=warn 59 | unsafe-getters-setters=warn 60 | inexact-spread=warn 61 | unnecessary-invariant=warn 62 | signature-verification-failure=warn 63 | deprecated-utility=error 64 | 65 | [strict] 66 | deprecated-type 67 | nonstrict-import 68 | sketchy-null 69 | unclear-type 70 | unsafe-getters-setters 71 | untyped-import 72 | untyped-type-import 73 | 74 | [version] 75 | ^0.105.0 76 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | yarn-error.log 37 | 38 | # BUCK 39 | buck-out/ 40 | \.buckd/ 41 | *.keystore 42 | !debug.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # CocoaPods 59 | /ios/Pods/ 60 | 61 | # Project exclude paths 62 | /android/ 63 | /ios/ 64 | /out/ 65 | /app/ 66 | 67 | .shadow-cljs -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: false, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | }; 7 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hackmd: https://hackmd.io/@byc70E6fQy67hPMN0WM9_A/rJilnJxE8 2 | 3 | # Confidence and Joy: React Native Development with ClojureScript and re-frame 4 | 5 | Clojure: https://clojure.org/guides/getting_started 6 | 7 | Code editor: IntelliJ IDEA Community https://www.jetbrains.com/idea/download/ 8 | with Cursive plugin https://cursive-ide.com/ 9 | 10 | shadow-cljs: http://shadow-cljs.org/ 11 | re-frame-steroid: https://github.com/flexsurfer/re-frame-steroid 12 | rn-shadow-steroid: https://github.com/flexsurfer/rn-shadow-steroid 13 | 14 | PROJECT SOURCES: https://github.com/flexsurfer/ClojureRNProject 15 | 16 | ### 1. Create a new React Native Project or open existing one 17 | 18 | `react-native init ClojureRNProject` 19 | 20 | `cd ClojureRNProject` 21 | 22 | Open project in IDE 23 | 24 | ![](https://i.imgur.com/GFLzmOi.png) 25 | 26 | Edit `App.js` 27 | 28 | ```jsx= 29 | import React from 'react'; 30 | import { 31 | SafeAreaView, 32 | View, 33 | Text, 34 | } from 'react-native'; 35 | 36 | const App: () => React$Node = () => { 37 | return ( 38 | <> 39 | 40 | 41 | Hello CLojure! 42 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default App; 49 | ``` 50 | 51 | Run the app 52 | 53 | Terminal 1: `yarn start` 54 | Terminal 2: `yarn ios` 55 | 56 | ![](https://i.imgur.com/uO6xvCK.png) 57 | 58 | 59 | OK, now we have RN project and we want to run the same app but with clojure 60 | 61 | 62 | ### 2. Add shadow-cljs 63 | 64 | `yarn add shadow-cljs` 65 | 66 | If you already have it, make sure you are using the latest version 67 | 68 | Create `shadow-cljs.edn` 69 | 70 | ```clojure 71 | {:source-paths ["src"] 72 | 73 | :dependencies [[reagent "0.10.0"] 74 | [re-frame "0.12.0"] 75 | [re-frame-steroid "0.1.1"] 76 | [rn-shadow-steroid "0.2.1"] 77 | [re-frisk-remote "1.3.3"]] 78 | 79 | :builds {:dev 80 | {:target :react-native 81 | :init-fn clojurernproject.core/init 82 | :output-dir "app" 83 | :compiler-options {:closure-defines 84 | {"re_frame.trace.trace_enabled_QMARK_" true}} 85 | :devtools {:after-load steroid.rn.core/reload 86 | :build-notify steroid.rn.core/build-notify 87 | :preloads [re-frisk-remote.preload]}}}} 88 | ``` 89 | 90 | Next, we need to initialize project as Clojure Deps, `deps.edn` will be used only for code inspection in IDE, if you know a better way pls file a PR 91 | 92 | ### 3. Create cljs project 93 | 94 | create `deps.edn` file 95 | 96 | ```clojure 97 | {:deps {org.clojure/clojure {:mvn/version "1.10.0"} 98 | org.clojure/clojurescript {:mvn/version "1.10.339"} 99 | reagent {:mvn/version "0.10.0"} 100 | re-frame {:mvn/version "0.12.0"} 101 | re-frame-steroid {:mvn/version "0.1.1"} 102 | rn-shadow-steroid {:mvn/version "0.2.1"}} 103 | :paths ["src"]} 104 | ``` 105 | 106 | Right click on the file and `Add as Clojure Deps Project` 107 | 108 | ![](https://i.imgur.com/C110quU.png) 109 | 110 | Optional turn off a spelling 111 | 112 | Indellij IDEA -> Preferences 113 | 114 | ![](https://i.imgur.com/eqWzrqM.png) 115 | 116 | create `src` folder and `clojurernproject` package with `core.cljs` file 117 | 118 | ![](https://i.imgur.com/gDEWfL3.png) 119 | 120 | 121 | core.cljs 122 | 123 | ```clojure 124 | (ns clojurernproject.core 125 | (:require [steroid.rn.core :as rn])) 126 | 127 | (defn root-comp [] 128 | [rn/safe-area-view 129 | [rn/view 130 | [rn/text "Hello CLojure! from CLJS"]]]) 131 | 132 | (defn init [] 133 | (rn/register-reload-comp "ClojureRNProject" root-comp)) 134 | 135 | ``` 136 | 137 | index.js 138 | 139 | ```javascript= 140 | import "./app/index.js"; 141 | ``` 142 | 143 | Terminal 3: `shadow-cljs watch dev` 144 | 145 | Reload the app 146 | 147 | **Disable Fast Refresh** 148 | 149 | Cmnd+D 150 | 151 | ![](https://i.imgur.com/7sOO4Ko.png) 152 | 153 | Now try to change the code, you should see it reloaded by shadow-cljs in the app 154 | 155 | now you have clojurescript RN app configured with hot reload 156 | 157 | 158 | ### 4. App state with re-frame 159 | 160 | To update app state, we need to use events, let's create `events.cljs` and register our first events 161 | 162 | events.cljs 163 | ```clojure 164 | (ns clojurernproject.events 165 | (:require [steroid.fx :as fx])) 166 | 167 | (fx/defn 168 | init-app-db 169 | {:events [:init-app-db]} 170 | [_] 171 | {:db {:counter 0}}) 172 | 173 | (fx/defn 174 | update-counter 175 | {:events [:update-counter]} 176 | [{:keys [db]}] 177 | {:db (update db :counter inc)}) 178 | ``` 179 | 180 | Set cursor on `fx/defn` and press `option+return` 181 | 182 | ![](https://i.imgur.com/4ahMkVJ.png) 183 | 184 | Move selection to `Resolve .. as...` and press `return` then select `defn` 185 | 186 | To update a view when the state is changed, we need to use subscriptions, let's create `subs.cljs` and register subscriptions. 187 | 188 | subs.cljs 189 | ```clojure 190 | (ns clojurernproject.subs 191 | (:require [steroid.subs :as subs])) 192 | 193 | (subs/reg-root-subs #{:counter}) 194 | ``` 195 | 196 | Now we can update our view 197 | 198 | core.cljs 199 | ```clojure 200 | (ns clojurernproject.core 201 | (:require [steroid.rn.core :as rn] 202 | [steroid.views :as views] 203 | [re-frame.core :as re-frame] 204 | clojurernproject.events 205 | clojurernproject.subs)) 206 | 207 | (views/defview root-comp [] 208 | (views/letsubs [counter [:counter]] 209 | [rn/safe-area-view {:style {:flex 1}} 210 | [rn/view {:style {:align-items :center :justify-content :center :flex 1}} 211 | [rn/text (str "Counter: " counter)] 212 | [rn/touchable-opacity {:on-press #(re-frame/dispatch [:update-counter])} 213 | [rn/view {:style {:background-color :gray :padding 5}} 214 | [rn/text "Update counter"]]]]])) 215 | 216 | (defn init [] 217 | (re-frame/dispatch [:init-app-db]) 218 | (rn/register-reload-comp "ClojureRNProject" root-comp)) 219 | ``` 220 | 221 | Resolve `defview` as `defn` and `letsubs` as `let` same way how we did it for `fx/defn` 222 | 223 | you can press "Update counter" button, and then change your code, and you can see app updated, but app state remained the same 224 | 225 | ![](https://i.imgur.com/T5wfvnX.png) 226 | 227 | now you have clojurescript RN app configured with hot reload and re-frame state 228 | 229 | There are three major rules when working with re-frame 230 | 1) views are pure and dumb, just render UI with data from subscriptions and dispatch events 231 | 232 | Bad: 233 | ```clojure 234 | (views/defview comp [] 235 | (views/letsubs [counter [:counter] 236 | delta [:delta]] 237 | [rn/text (str "Counter: " (+ counter delta))] 238 | [rn/touchable-opacity 239 | {:on-press #(re-frame/dispatch 240 | [:update-counter (if (> delta 12) 241 | counter 242 | delta)])}])) 243 | ``` 244 | 245 | Good: 246 | ```clojure 247 | (views/defview comp [] 248 | (views/letsubs [counter-with-delta [:counter-with-delta]] 249 | [rn/text (str "Counter: " counter-with-delta)] 250 | [rn/touchable-opacity 251 | {:on-press #(re-frame/dispatch [:update-counter])}])) 252 | ``` 253 | we have a separate subscription and event will get all data from the state 254 | 255 | 2. Only root keys should be subscribed on app-db 256 | 257 | Bad: 258 | ```clojure 259 | (re-frame/reg-sub :counter (fn [db] (get db :counter))) 260 | 261 | (re-frame/reg-sub :delta (fn [db] (get db :delta))) 262 | 263 | (re-frame/reg-sub :counter-with-delta (fn [db] (+ (get db :counter) (get db :delta))) 264 | ``` 265 | 266 | Good: 267 | 268 | ```clojure 269 | (subs/reg-root-subs #{:counter :delta}) 270 | 271 | (re-frame/reg-sub 272 | :counter-with-delta 273 | :<- [:counter] 274 | :<- [:delta] 275 | (fn [[counter delta]] 276 | (+ counter delta))) 277 | ``` 278 | 279 | 3. Events must be pure and do all computations 280 | 281 | Bad: 282 | ```clojure 283 | (fx/defn 284 | update-counter 285 | {:events [:update-counter]} 286 | [{:keys [db]}] 287 | (do-something) 288 | {:db (update db :counter inc)}) 289 | ``` 290 | 291 | Good: 292 | ```clojure 293 | (re-frame/reg-fx 294 | :do-something 295 | (fn [] 296 | (do-something))) 297 | 298 | (fx/defn 299 | update-counter 300 | {:events [:update-counter]} 301 | [{:keys [db]}] 302 | {:db (update db :counter inc) 303 | :do-something nil}) 304 | ``` 305 | 306 | ### 6. Devtools 307 | 308 | let's run re-frisk debugging tool and see what's exactly happening in the app 309 | 310 | Terminal 4: `shadow-cljs run re-frisk-remote.core/start` 311 | 312 | and open `http://localhost:4567` 313 | 314 | ![](https://i.imgur.com/6ty7nbr.png) 315 | 316 | You can see all that is happening with the app: events, app-db (state) and subscriptions 317 | 318 | ### 6. Tests 319 | 320 | Add test folder and configure test build in the project 321 | 322 | ```clojure 323 | {:source-paths ["src" "test"] 324 | 325 | :dependencies [[...]] 326 | 327 | :builds {:dev 328 | {...} 329 | 330 | :test 331 | {:target :node-test 332 | :output-to "out/node-tests.js" 333 | :autorun true}}} 334 | ``` 335 | 336 | Let's add some tests 337 | 338 | events/counter_test.cljs 339 | 340 | ```clojure 341 | (ns events.counter-test 342 | (:require [cljs.test :refer (deftest is)] 343 | [clojurernproject.events :as events])) 344 | 345 | (deftest events-counter-test 346 | (is (= (events/update-counter {:db {:counter 0}}) 347 | {:db {:counter 1}}))) 348 | ``` 349 | 350 | And run tests 351 | 352 | Terminal 3: `shadow-cljs compile test` 353 | 354 | ![](https://i.imgur.com/28gspBL.png) 355 | 356 | ### 7. Navigation 357 | 358 | React Navigation 5 359 | 360 | Terminal 2: `yarn add @react-navigation/native @react-navigation/stack @react-navigation/bottom-tab react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view` 361 | 362 | Terminal 2: `cd ios; pod install; cd ..` 363 | 364 | Terminal 2: `yarn ios` 365 | 366 | core.cljs 367 | ```clojure 368 | (ns clojurernproject.core 369 | (:require [steroid.rn.core :as rn] 370 | [re-frame.core :as re-frame] 371 | [steroid.rn.navigation.core :as rnn] 372 | [steroid.rn.navigation.stack :as stack] 373 | [steroid.rn.navigation.bottom-tabs :as bottom-tabs] 374 | [clojurernproject.views :as screens] 375 | [steroid.rn.navigation.safe-area :as safe-area] 376 | steroid.rn.navigation.events 377 | clojurernproject.events 378 | clojurernproject.subs)) 379 | 380 | (defn main-screens [] 381 | [bottom-tabs/bottom-tab 382 | [{:name :home 383 | :component screens/home-screen} 384 | {:name :basic 385 | :component screens/basic-screen} 386 | {:name :ui 387 | :component screens/ui-screen} 388 | {:name :list 389 | :component screens/list-screen} 390 | {:name :storage 391 | :component screens/storage-screen}]]) 392 | 393 | (defn root-stack [] 394 | [safe-area/safe-area-provider 395 | [(rnn/create-navigation-container-reload 396 | {:on-ready #(re-frame/dispatch [:init-app-db])} 397 | [stack/stack {:mode :modal :header-mode :none} 398 | [{:name :main 399 | :component main-screens} 400 | {:name :modal 401 | :component screens/modal-screen}]])]]) 402 | 403 | (defn init [] 404 | (rn/register-comp "ClojureRNProject" root-stack)) 405 | ``` 406 | 407 | For hot reload we need to register components differently, we register `root-stack` as regular not reloadable component `rn/register-comp` but we use `rnn/create-navigation-container-reload` for navigation container 408 | 409 | After we've required `steroid.rn.navigation.events` ns we can dispatch `:navigate-to` and `:navigate-back` events for navigation between screens 410 | 411 | Try to open modal screen and change the code you will see that navigation state isn't changed, the modal screen will be still opened 412 | 413 | ![IMG](https://github.com/flexsurfer/rn-shadow-steroid/raw/master/screencast.gif) 414 | 415 | КОНЕЦ -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ClojureRNProject", 3 | "displayName": "ClojureRNProject" 4 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.10.0"} 2 | org.clojure/clojurescript {:mvn/version "1.10.339"} 3 | reagent/reagent {:mvn/version "0.10.0"} 4 | re-frame/re-frame {:mvn/version "0.12.0"} 5 | re-frame-steroid/re-frame-steroid {:mvn/version "0.1.1"} 6 | rn-shadow-steroid/rn-shadow-steroid {:mvn/version "0.2.1"} 7 | re-frisk-remote/re-frisk-remote {:mvn/version "1.3.3"}} 8 | :paths ["src" "test"]} 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import "./app/index.js"; -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ClojureRNProject", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "shadow-cljs watch dev", 7 | "android": "react-native run-android", 8 | "ios": "react-native run-ios", 9 | "start": "react-native start", 10 | "test": "jest", 11 | "lint": "eslint .", 12 | "re-frisk": "shadow-cljs run re-frisk-remote.core/start" 13 | }, 14 | "dependencies": { 15 | "@react-native-community/async-storage": "^1.10.1", 16 | "@react-native-community/datetimepicker": "^2.4.0", 17 | "@react-native-community/masked-view": "^0.1.6", 18 | "@react-native-community/picker": "^1.5.0", 19 | "@react-navigation/bottom-tabs": "^5.4.5", 20 | "@react-navigation/native": "^5.0.7", 21 | "@react-navigation/stack": "^5.0.9", 22 | "react": "16.9.0", 23 | "react-dom": "16.9.0", 24 | "react-native": "0.61.5", 25 | "react-native-gesture-handler": "^1.6.0", 26 | "react-native-reanimated": "^1.7.0", 27 | "react-native-safe-area-context": "^1.0.2", 28 | "react-native-screens": "^2.0.0-beta.10", 29 | "shadow-cljs": "^2.9.6", 30 | "websocket": "^1.0.31" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.8.4", 34 | "@babel/runtime": "^7.8.4", 35 | "@react-native-community/eslint-config": "^0.0.7", 36 | "babel-jest": "^25.1.0", 37 | "eslint": "^6.8.0", 38 | "jest": "^25.1.0", 39 | "metro-react-native-babel-preset": "^0.58.0", 40 | "react-test-renderer": "16.9.0" 41 | }, 42 | "jest": { 43 | "preset": "react-native" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src" "test"] 2 | 3 | :dependencies [[reagent "1.0.0"] 4 | [re-frame "0.12.0"] 5 | [re-frame-steroid "0.1.1"] 6 | [rn-shadow-steroid "0.2.8"] 7 | [re-frisk-remote "1.5.0"]] 8 | 9 | :builds {:dev 10 | {:target :react-native 11 | :init-fn clojurernproject.core/init 12 | :output-dir "app" 13 | :compiler-options {:closure-defines 14 | {"re_frame.trace.trace_enabled_QMARK_" true}} 15 | :devtools {:after-load steroid.rn.core/reload 16 | :build-notify steroid.rn.core/build-notify 17 | :preloads [re-frisk-remote.preload]}} 18 | 19 | :test 20 | {:target :node-test 21 | :output-to "out/node-tests.js" 22 | :autorun true}}} -------------------------------------------------------------------------------- /src/clojurernproject/core.cljs: -------------------------------------------------------------------------------- 1 | (ns clojurernproject.core 2 | (:require [steroid.rn.core :as rn] 3 | [re-frame.core :as re-frame] 4 | [steroid.rn.navigation.core :as rnn] 5 | [steroid.rn.navigation.stack :as stack] 6 | [steroid.rn.navigation.bottom-tabs :as bottom-tabs] 7 | [clojurernproject.views :as screens] 8 | [steroid.rn.navigation.safe-area :as safe-area] 9 | steroid.rn.navigation.events 10 | clojurernproject.events 11 | clojurernproject.subs)) 12 | 13 | (defn main-screens [] 14 | [bottom-tabs/bottom-tab 15 | [{:name :home 16 | :component screens/home-screen} 17 | {:name :basic 18 | :component screens/basic-screen} 19 | {:name :ui 20 | :component screens/ui-screen} 21 | {:name :list 22 | :component screens/list-screen} 23 | {:name :storage 24 | :component screens/storage-screen}]]) 25 | 26 | (defn root-stack [] 27 | [safe-area/safe-area-provider 28 | [(rnn/create-navigation-container-reload 29 | {:on-ready #(re-frame/dispatch [:init-app-db])} 30 | [stack/stack {:mode :modal :header-mode :none} 31 | [{:name :main 32 | :component main-screens} 33 | {:name :modal 34 | :component screens/modal-screen}]])]]) 35 | 36 | (defn init [] 37 | (rn/register-comp "ClojureRNProject" root-stack)) -------------------------------------------------------------------------------- /src/clojurernproject/events.cljs: -------------------------------------------------------------------------------- 1 | (ns clojurernproject.events 2 | (:require [steroid.fx :as fx])) 3 | 4 | (fx/defn 5 | init-app-db 6 | {:events [:init-app-db]} 7 | [_] 8 | {:db {:counter 0 9 | :delta 10}}) 10 | 11 | (fx/defn 12 | update-counter 13 | {:events [:update-counter]} 14 | [{:keys [db]}] 15 | {:db (update db :counter inc)}) -------------------------------------------------------------------------------- /src/clojurernproject/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns clojurernproject.subs 2 | (:require [steroid.subs :as subs] 3 | [re-frame.core :as re-frame])) 4 | 5 | (subs/reg-root-subs #{:counter :delta}) 6 | 7 | (re-frame/reg-sub 8 | :counter-with-delta 9 | :<- [:counter] 10 | :<- [:delta] 11 | (fn [[counter delta]] 12 | (+ counter delta))) -------------------------------------------------------------------------------- /src/clojurernproject/views.cljs: -------------------------------------------------------------------------------- 1 | (ns clojurernproject.views 2 | (:require [steroid.rn.core :as rn] 3 | [steroid.views :as views] 4 | [re-frame.core :as re-frame] 5 | [steroid.rn.components.ui :as ui] 6 | [reagent.core :as reagent] 7 | [steroid.rn.navigation.safe-area :as safe-area] 8 | #_[steroid.rn.components.picker :as picker] 9 | [steroid.rn.components.list :as list] 10 | #_[steroid.rn.components.async-storage :as async-storage] 11 | #_[steroid.rn.components.datetimepicker :as datetimepicker] 12 | [clojure.string :as string])) 13 | 14 | (views/defview home-screen [] 15 | (views/letsubs [counter [:counter-with-delta]] 16 | [safe-area/safe-area-view {:style {:flex 1}} 17 | [rn/view {:style {:align-items :center :justify-content :center :flex 1}} 18 | [rn/text {:style {:margin-bottom 20}} 19 | (str "Counter with delta: " counter)] 20 | [ui/button {:title "Update counter" 21 | :on-press #(re-frame/dispatch [:update-counter])}] 22 | [rn/view {:style {:height 20}}] 23 | [ui/button {:title "Basic components" 24 | :on-press #(re-frame/dispatch [:navigate-to :basic])}] 25 | [ui/button {:title "UI components" 26 | :on-press #(re-frame/dispatch [:navigate-to :ui])}] 27 | [ui/button {:title "List" 28 | :on-press #(re-frame/dispatch [:navigate-to :list])}] 29 | [ui/button {:title "Storage" 30 | :on-press #(re-frame/dispatch [:navigate-to :storage])}] 31 | [ui/button {:title "Open modal" 32 | :on-press #(re-frame/dispatch [:navigate-to :modal])}]]])) 33 | 34 | (defn modal-screen [] 35 | [rn/view {:style {:align-items :center :justify-content :center :flex 1}} 36 | [ui/button {:title "Navigate back" 37 | :on-press #(re-frame/dispatch [:navigate-back])}]]) 38 | 39 | (defn basic-screen [] 40 | [safe-area/safe-area-view 41 | [rn/view {:style {:padding 20}} 42 | ;;VIEW 43 | [rn/view {:style {:width 300 :height 20 :background-color :blue}}] 44 | ;;TEXT 45 | [rn/text {:style {:color :red :font-size 30 :margin-top 10}} "Text2"] 46 | ;;IMAGE 47 | [rn/image {:style {:width 50 :height 50 :margin-top 10} 48 | :source {:uri "https://reactnative.dev/img/tiny_logo.png"}}] 49 | ;;TEXT INPUT 50 | [rn/text-input {:style {:height 40 :borderColor :gray :border-width 1 51 | :margin-top 10} 52 | :placeholder "Type your text ..."}] 53 | ;;SCROLL VIEW 54 | [rn/scroll-view {:style {:height 100 :margin-top 10}} 55 | (for [i (range 20)] 56 | ^{:key (str "item" i)} 57 | [rn/view {:style {:margin-bottom 5 :width 300 :height 20 58 | :background-color (str "#" (.toString (rand-int 16rFFFFFF) 16))}}])]]]) 59 | 60 | (defn ui-screen [] 61 | (let [enabled? (reagent/atom false) 62 | picker-value (reagent/atom "clj")] 63 | (fn [] 64 | [safe-area/safe-area-view 65 | [rn/view {:style {:padding 20}} 66 | ;;BUTTON 67 | [ui/button {:title "Button"}] 68 | [ui/button {:title "Button" :color :red}] 69 | ;;SWITCH 70 | [ui/switch {:on-value-change #(swap! enabled? not) 71 | :value @enabled?}] 72 | [rn/text {:style {:margin-top 10}} 73 | (str "Switch ebabled: " @enabled?)] 74 | ;;PICKER 75 | [rn/text "add \"@react-native-community/picker\" and uncomment the code"] 76 | #_[picker/picker {:style {:height 250 :width 200} 77 | :selected-value @picker-value 78 | :on-value-change #(reset! picker-value %)} 79 | [picker/item {:label "Clojure" :value "clj"}] 80 | [picker/item {:label "ClojureScript" :value "cljs"}] 81 | [picker/item {:label "Java" :value "java"}] 82 | [picker/item {:label "JavaScript" :value "js"}]] 83 | [rn/text (str "Selected item: " @picker-value)] 84 | ;;DATETIME PICKER 85 | [rn/text "add \"@react-native-community/datetimepicker\" and uncomment the code"] 86 | #_[datetimepicker/date-time-picker 87 | {:testID "dateTimePicker" 88 | :timeZoneOffsetInMinutes 0 89 | :value (js/Date. 1598051730000) 90 | :is24Hour true 91 | :display "default"}]]]))) 92 | 93 | (defn flat-list-renderer [{:keys [title value]}] 94 | [rn/text (str title " " value)]) 95 | 96 | (defn section-list-renderer [{:keys [title value]}] 97 | [rn/text (str title " " value)]) 98 | 99 | (defn list-screen [] 100 | [safe-area/safe-area-view 101 | [rn/view {:style {:padding 20}} 102 | ;;FLAT LIST 103 | [list/flat-list {:style {:height 100} 104 | :data (for [i (range 20)] 105 | {:title "item" 106 | :value i}) 107 | :render-fn flat-list-renderer}] 108 | ;;SECTION LIST 109 | [list/section-list {:style {:height 100 110 | :margin-top 50} 111 | :sections [{:title "Title 1" 112 | :key :data1 113 | :data (for [i (range 10)] 114 | {:title "item 1" 115 | :value i})} 116 | {:title "Title 2" 117 | :key :data2 118 | :data (for [i (range 10)] 119 | {:title "item 2" 120 | :value i})} 121 | {:title "Title 3" 122 | :key :data3 123 | :data (for [i (range 10)] 124 | {:title "item 3" 125 | :value i})}] 126 | :render-fn section-list-renderer}]]]) 127 | 128 | (defn storage-screen [] 129 | (let [read-value (reagent/atom "") 130 | new-value (reagent/atom "")] 131 | (fn [] 132 | [safe-area/safe-area-view 133 | [rn/text "add \"@react-native-community/async-storage\" and uncomment the code"] 134 | #_[rn/view {:style {:padding 20}} 135 | [ui/button {:title "Read value" 136 | :on-press (fn [] 137 | (async-storage/get-item "my-key" #(reset! read-value %)))}] 138 | [rn/text (str "Read value " @read-value)] 139 | [rn/text-input {:style {:height 40 :margin-top 10 140 | :borderColor :gray :border-width 1} 141 | :on-change-text #(reset! new-value %) 142 | :placeholder "Type your text ..."}] 143 | [ui/button {:title "Write value" 144 | :disabled (string/blank? @new-value) 145 | :on-press #(async-storage/set-item "my-key" @new-value)}]]]))) 146 | -------------------------------------------------------------------------------- /test/events/counter_test.cljs: -------------------------------------------------------------------------------- 1 | (ns events.counter-test 2 | (:require [cljs.test :refer (deftest is)] 3 | [clojurernproject.events :as events])) 4 | 5 | (deftest events-counter-test 6 | (is (= (events/update-counter {:db {:counter 0}}) 7 | {:db {:counter 1}}))) --------------------------------------------------------------------------------