{"id":13760407,"url":"https://github.com/ingesolvoll/kee-frame","last_synced_at":"2026-01-24T03:28:30.463Z","repository":{"id":37579770,"uuid":"121682914","full_name":"ingesolvoll/kee-frame","owner":"ingesolvoll","description":"re-frame with batteries included","archived":false,"fork":false,"pushed_at":"2023-09-20T10:51:41.000Z","size":481,"stargazers_count":353,"open_issues_count":7,"forks_count":25,"subscribers_count":12,"default_branch":"master","last_synced_at":"2024-08-03T13:04:49.745Z","etag":null,"topics":["clojure","clojurescript","keechma","re-frame","react","reagent"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ingesolvoll.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-02-15T21:07:20.000Z","updated_at":"2024-05-31T07:40:40.000Z","dependencies_parsed_at":"2024-01-15T03:59:42.863Z","dependency_job_id":null,"html_url":"https://github.com/ingesolvoll/kee-frame","commit_stats":null,"previous_names":[],"tags_count":30,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ingesolvoll%2Fkee-frame","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ingesolvoll%2Fkee-frame/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ingesolvoll%2Fkee-frame/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ingesolvoll%2Fkee-frame/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ingesolvoll","download_url":"https://codeload.github.com/ingesolvoll/kee-frame/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224949814,"owners_count":17397239,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","clojurescript","keechma","re-frame","react","reagent"],"created_at":"2024-08-03T13:01:09.705Z","updated_at":"2026-01-24T03:28:30.418Z","avatar_url":"https://github.com/ingesolvoll.png","language":"Clojure","funding_links":[],"categories":["Clojure"],"sub_categories":[],"readme":"# kee-frame\n\n[re-frame](https://github.com/Day8/re-frame) with batteries included.  \n\n[![Build Status](https://travis-ci.org/ingesolvoll/kee-frame.svg?branch=master)](https://travis-ci.org/ingesolvoll/kee-frame)\n\n[![Clojars Project](https://img.shields.io/clojars/v/kee-frame.svg)](https://clojars.org/kee-frame)\n\n[![cljdoc badge](https://cljdoc.xyz/badge/kee-frame/kee-frame)](https://cljdoc.xyz/d/kee-frame/kee-frame/CURRENT)\n\n[![project chat](https://img.shields.io/badge/slack-join_chat-brightgreen.svg)](https://clojurians.slack.com/archives/CA7EJ0Y2U)\n\n\n## Project status (August 2021)\n\nThe API and functionality of kee-frame is stable and proven to work.\nPull requests are welcome!\n \n## Quick walkthrough\n- If you prefer, you can go straight to some [articles](http://ingesolvoll.github.io/tags/kee-frame/) or the [demo app](https://github.com/ingesolvoll/kee-frame-sample)\n\n- Require core namespace\n```clojure\n(require '[kee-frame.core :as k])\n```\n\n\n- Start your re-frame app and mount it into the DOM with sensible defaults for routing, logging, spec validation, etc.\nCall this function on figwheel reload.\n\n```clojure\n(k/start!  {:routes         [[\"/\" :live]\n                             [\"/league/:id/:tab\" :league]]\n            :app-db-spec    :my-app/db-spec\n            :initial-db     {:some-prop true}\n            :root-component [my-root-reagent-component]\n            :debug?         true})\n```\n\n- Declare that you want some data to be loaded when the user navigates to the league page\n```clojure      \n(k/reg-controller :league\n                  {:params (fn [route-data]\n                             (when (-\u003e route-data :data :name (= :league))\n                               (-\u003e route-data\n                                   :path-params\n                                   :id)))\n                   :start  (fn [ctx id] [:league/load id])})\n```\n\n- Declare how to get those data from the server\n```clojure      \n(k/reg-chain :league/load\n            \n             (fn [ctx [id]]\n               {:http-xhrio {:method          :get\n                             :uri             (str \"/leagues/\" id)}})\n            \n             (fn [{:keys [db]} [_ league-data]]\n               {:db (assoc db :league league-data)}))\n```\n\n- You can use a Finite State Machine to handle error paths and complexity, with or without controllers and chains. See FSM doc section for details.\n\n- Make a URL for your `\u003ca href=\"\"\u003e` using nothing but data\n```clojure\n(k/path-for [:league {:id 14 :tab :fixtures}]) =\u003e \"/league/14/fixtures\"\n```\n\n- Let your event handler trigger browser navigation as a side effect, using nothing but data\n```clojure      \n(reg-event-fx :todo-added\n              (fn [_ [todo]]\n                {:navigate-to [:todo {:id (:id todo)}]]}))\n```\n\n- Let the route data decide what view to display\n```clojure\n(defn main-view []\n  [k/switch-route (fn [route] (-\u003e route :data :name))\n   :index [index-page] \n   :orders [orders-page]\n   nil [:div \"Loading...\"])\n```\n\n## Benefits of leaving the URL in charge\n\nKee-frame wants you to focus on the URL and let it contain all data necessary to load a view. When you let \nthe URL guide you app architecture like this, strange things start to happen: \n* Back/forward and bookmarking in general just works. The internet is back!\n* Figwheel reloading gets even better\n* UI code gets more declarative\n* Cohesion goes up, coupling goes down\n\n\n## That's it!\nYou've reached the end of the quick summary. Keep reading for a more in-depth guide!\n\n## Articles\n\n[Learning kee-frame in 5 minutes](http://ingesolvoll.github.io/posts/2018-04-01-learning-kee-frame-in-5-minutes/)\n\n[Introduction and background for kee-frame controllers](http://ingesolvoll.github.io/posts/2018-04-01-kee-frame-putting-the-url-in-charge/)\n\n[Controller tricks](http://ingesolvoll.github.io/posts/2018-06-18-kee-frame-controller-tricks/)\n\n## Demo application\nI made a simple demo app showing football results. Have a look around, and observe how all data loading just works while navigating and refreshing the page.\n\n[Online demo app](http://kee-frame-sample.herokuapp.com/) \n\n[Demo app source](https://github.com/ingesolvoll/kee-frame-sample)\n\nFeel free to clone the demo app and do some figwheelin' with it!\n\n## Support\nContact the author on [Twitter](https://twitter.com/ingesol) or join the discussion on [Slack](https://clojurians.slack.com/messages/kee-frame). Don't be afraid to create [issues](https://github.com/ingesolvoll/kee-frame/issues). Lack of user friendliness is also a bug!\n\n## Installation\nThere are 2 simple options for bootstrapping your project:\n\n### 1. Manual installation\nAdd the following dependency to your `project.clj` file:\n```clojure\n[kee-frame \"1.2.0\"]\n```\n### 2. Luminus template\n[Luminus](http://www.luminusweb.net) is a framework that makes it easy to get started with web app development\nin clojure. It comes with kee-frame if you do this:\n\n```\nlein new luminus your-app-name-here +kee-frame\n``` \n\n## API stability\nThis library tries hard conform to the high standards of many Clojure libraries, by not breaking backwards compatibility.\nI believe this is very important, an application made several years ago should be able to upgrade with close to zero effort.\n\nThe kee-frame API has remained stable since the launch in early 2018. Here is a list of important/breaking changes:\n* 0.3.0: Reitit replaces Bidi as the default routing library. Causes a breaking change in the data structures of routes and route matches. [The bidi router implementation can be found here, it's easy to fit back in.](https://github.com/ingesolvoll/kee-frame-sample/blob/master/src/cljs/kee_frame_sample/routers.cljs)\n* 0.3.4: Changes in log configuration, might render unexpected results in more/less logging for existing projects.\n* 0.4.0: FSM API introduced. No breaking API change, additions only.\n* 0.4.0: Websocket API moved out to separate library, to reduce bundle size. Requires separate dependency.\n\n## Getting started\n\nThe `kee-frame.core` namespace contains the public API. It also contains wrapped versions of `reg-event-db` and `reg-event-fx`.\n\n```clojure\n(require '[kee-frame.core :as k :refer [reg-controller reg-chain reg-event-db reg-event-fx]])\n```\n\n## Routes\nKee-frame uses [reitit](https://github.com/metosin/reitit) for routing (since 0.3.0). Read the reitit docs for more details on the syntax, here are the routes from the demo app:\n\n```clojure\n(def routes [[\"/\" :live]\n             [\"/league/:id/:tab\" :league]])\n```\n\n## Starting your app\nThe `start!` function starts the router and configures the application.\n\n```clojure\n(k/start!  {:routes         my-routes\n            :app-db-spec    :my-app/db-spec\n            :initial-db     your-blank-db-map\n            :root-component [my-root-reagent-component]\n            :debug?         true})\n```\n\nSubsequent calls to start are not a problem, so call this function as often as you want. Typically on every figwheel reload.\n\nThe `routes` property causes kee-frame to wire up the browser to navigate by those routes. Skip this property if you want to do your own routing. See the \"Introducing kee-frame into an existing app\" section.\n\nYou can set the `hash-routing?` property to `true` for `/#/todos/1` style urls. Otherwise kee-frame defaults to using the browser\nhistory without the hash. The hash bit should not be included in your route definition, kee-frame strips it off before matching\nthe route.\n\nBy default an unknown URL/route causes an error. You can provide a string URL under config key `:not-found` \nas your default 404 when no route is found.\n\nIf you provide `:root-component`, kee-frame will render that component in the DOM element with id \"app\". Make sure you have such an element in your index.html. You are free to do the initial rendering yourself if you want, just skip this setting. If you use this feature, make sure that `k/start!` is called every time figwheel reloads your code. \n\nThe `:log` option accepts a timbre log configuration map. See the [Logging](#logging) section for more details.\n\nThe `debug?` and `debug-config` options were replaced by the `:log` option in 0.4.1. \n\nIf you provide an `app-db-spec`, the framework will let you know when a bug in your event handler is trying to corrupt your DB structure. This is incredibly useful, so you should put down the effort to spec up your db!\n\nYou can override kee-frame's behaviour on route change through the `:route-change-event` option.\nJust specify the id of the event you want to use. One possible case is to perform some gatekeeping\nbefore executing the controllers for that route. If route execution is ok, \nthe event could dispatch to kee-frame's built in `:kee-frame.router/route-changed`.\n\n## Controllers\nA controller is a connection between the route data and your event handlers. It is a map with two required keys (`params` and `start`), and one optional (`stop`).\n\nThe `params` function receives the route data every time the URL changes. Its only job is to return the part of the route that it's interested in. This value combined with the previous value decides the next state of the controller. I'll come back to that in more detail.\n\nThe `start` function accepts the full re-frame context and the value returned from `params`. It should return nil or an event vector to be dispatched.\n\nThe `stop` function receives the re-frame context and also returns nil or an event vector.\n\n```clojure      \n(reg-controller :league\n                {:params (fn [{:keys [handler route-params]}]\n                           (when (= handler :league)\n                             (:id route-params)))\n                 :start  (fn [_ id]\n                           [:league/load id])})\n```\n\nFor `start` and `stop` it's very common to ignore the parameters and just return an event vector, and for that you can use a vector instead of a function:\n\n```clojure      \n(reg-controller :leagues\n                {:params (constantly true) ;; Will cause the controller to start immediately, but only once\n                 :start  [:leagues/load]}) ;; The route params will be appended to this vector, as the first event param.\n```\n\n## Controller state transitions\nThis rules of controller states are stolen entirely from Keechma. \n\nNOTE: `false` is not treated as falsy in controllers, it is considered as any other value. The explicitly negative value that stops controllers is `nil`.\n\nThe controller rules are:\n* When previous and current parameter values are the same, do nothing\n* When previous parameter was nil and current is not nil, call `start`.\n* When previous parameter was not nil and current is nil, call `stop`.\n* When both previous and current are not nil, but different, call `stop`, then `start`.\n\nAll controllers `:params` fns are called every time the route changes. Therefore `:params` should return `nil` for routes on which it should not be started.\n\n## Event chains\n\nKee-frame uses [re-chain](https://github.com/ingesolvoll/re-chain) to chain event handlers together for increased readability\nless boilerplate for common cases. See the example below for how to use it, visit the [re-chain](https://github.com/ingesolvoll/re-chain) \npage for details and documentation\n\n```clojure      \n(k/reg-chain :league/load\n            \n             (fn [ctx [id]]\n               {:http-xhrio {:method          :get\n                             :uri             (str \"/leagues/\" id)}})\n            \n             (fn [{:keys [db]} [_ league-data]]\n               {:db (assoc db :league league-data)}))\n```\n \n\n## Browser navigation\n\nUsing URL strings in your links and navigation is error prone and quickly becomes a maintenance problem. Therefore, kee-frame encourages you to only interact with route data instead of concrete URLs. It provides 2 abstractions to help you with that:\n\nThe `kee-frame.core/path-for` function accepts a reitit route and returns a URL string:\n\n`(k/path-for [:todos {:id 14}]) =\u003e \"/todos/14\"`\n\nKee-frame also includes a re-frame effect for triggering a browser navigation, after all navigation is a side effect. The effect is `:navigate-to` and it accepts a reitit route. The example below shows a handler that receives some data and navigates to the view page for those data.\n\n```clojure      \n(reg-event-fx :todo-added\n              (fn [_ [todo]]\n                {:db          (update db :todos conj todo)\n                 :navigate-to [:todo :id (:id todo)]]})) ;; \"/todos/14\"\n```\n\nSee [this issue](https://github.com/ingesolvoll/kee-frame/issues/64) for some hints on how to use query parameters in your browser navigation.\n\n## Routing in your views\n\nMost apps need to different views for different URLs. This isn't too hard to solve in re-frame, just subscribe to your route and implement your dispatch logic like this:\n\n```clojure      \n(defn main-view []\n  (let [route (subscribe [:kee-frame/route])]\n    (fn []\n      [:div\n       (case (:handler @route)\n         :index [index-page]\n         :orders [orders-page])])))\n```\n\nKee-frame provides a simple helper to do this:\n\n```clojure\n(defn main-view []\n  [k/switch-route (fn [route] (-\u003e route :data :name))\n   :index [index-page] ;; Explicit call to reagent component, ignoring route data\n   :orders orders-page]) ;; Orders page will receive the route data as its parameter because of missing []\n```\n\nIt looks pretty much the same, only more concise. But it does help you with a few subtle but important things:\n\n* Forces you into a known working pattern\n* No explicit reference to the route. The first argument to `switch-route` is a function that accepts the route and returns the value you are dispatching on\n* If you pass only a function reference to your reagent components (no surrounding []), kee-frame will invoke them with the route as the first parameter.\n* It will give you nice error messages when you make a mistake.\n\n## Finite State Machines\nThe last year there has been several experiments in kee-frame for integrating FSMs. I have now reached a conclusion\non this, and created [re-statecharts](https://github.com/ingesolvoll/re-statecharts). It is available in kee-frame\nthrough [glimt](https://github.com/ingesolvoll/glimt).\n\nIf you have been using event chains to do your HTTP requests, glimt is clearly a better option. It handles more than\njust the happy path and gives you with the state of the request along the way. I highly recommend trying out FSMs in \nyour apps!\n\n## Logging\n\nhttps://github.com/ptaoussanis/timbre is included for logging. The `kee-frame.core/start!` function accepts an optional timbre config map. The default\nconfig logs to browser console at level `:info`. Here's an example config you could pass in:\n\n```clojure\n{:log {:level        :debug\n       :ns-blacklist [\"kee-frame.event-logger\"]}\n...\n}\n```\n\nKee-frame uses debug logging as an aid in debugging applications. This means that turning on all logging\nwill get quite noisy. You might want to include some of these namespaces in your blacklist if you don't need them:\n- `kee-frame.event-logger`\n- `kee-frame.fsm.alpha`\n\nIMPORTANT: For debug logging to show up in Chrome you need to enable \"Verbose\" logging in Chrome dev tools. See https://github.com/ptaoussanis/timbre/issues/249\n\n\n## Introducing kee-frame into an existing app\n\nSeveral parts of kee-frame are designed to be opt-in. This means that you can include kee-frame in your project and start using parts of it.\n\nIf you want controllers and routes, you need to replace your current routing with kee-frame's routing. If your current routing requires a lot of work\n  to fit with the standard reitit routing solution, you may implement a custom router. See the next section for more details.\n\nAlternatively, make your current router dispatch the event `[:kee-frame.router/route-changed route-data]` on every route change. That should enable what you need for the controllers.\n\n## Using a different router implementation\n\nYou may not like reitit, or you are already using a different router. In that case, all you have to do is implement your own version of the protocol\n`kee-frame.api/Router` and pass it in with the rest of your config:\n\n```clojure\n(k/start!  {:router         (-\u003eBidiRouter bidi-style-routes)\n            :root-component [my-root-reagent-component]\n            ...})\n```\n\n[Here are some example (not fully tested) router implementations](https://github.com/ingesolvoll/kee-frame-sample/blob/master/src/cljs/kee_frame_sample/routers.cljs). If you are upgrading from a pre 0.3.0\nversion of kee-frame, you probably want to keep your current bidi routes. The old bidi router implementation can be found here, just make a copy\nand use it as your `:router`.\n\nIf you choose to use a different router than reitit, you also need to use the corresponding routing data format when using `path-for` and the `:navigate-to` effect.\n\n## Server side routes\nIf you want to use links without hashes (`/some-route` instead of `/#/some-route`), you need a bit of server setup for it to work perfectly. \nA React SPA is typically loaded from the `\"app\"` element inside `index.html` served from the root `/` of your server. \nIf the user navigates to some client route `/leagues/465` and then hits refresh, the server will be unable to match that \nroute as it exists only on the client. We will get a 404 instead of the `index.html` that we need. We want this to work, \nso that URLs can still be deterministic, even if they exist only on the client.\n\nYou can solve this in several ways, the simplest way is to include a wildcard route as the last route on the server. The server should serve `index.html` on any route not found on the server. This works, the downside is that you won't be able to serve a 404 page for non-matched URLs on the server. \n\nIn compojure, the wildcard route would look like this:\n\n```clojure\n(GET \"*\" req {:headers {\"Content-Type\" \"text/html\"}\n                  :status  200\n                  :body    (index-handler req)})\n```\n\n## Screen size breakpoints\n\nMost web apps benefit from having direct access to information about the size and orientation of the screen. Kee-frame\nships with the nice and simple [breaking-points](https://github.com/gadfly361/breaking-point) library that provides \nsubscriptions for the screen properties you're interested in.\n\nThe screen breakpoints are completely configurable, you can pass your preferred ones to the `start!` function. The ones\nlisted in the example below are the defaults, so if you're happy with those you can just pass `true` to the `:screen`\nparameter. If you omit it altogether, or pass `false` - the screen breakpoints will be disabled.\n\n```clojure\n(k/start!  {:screen {:breakpoints \n                        [:mobile\n                         768\n                         :tablet\n                         992\n                         :small-monitor\n                         1200\n                         :large-monitor]\n                     :debounce-ms 166}\n              ;; Other settings here\n              })\n```\n\nThe subscriptions available are:\n\n```clojure\n(rf/subscribe [:breaking-point.core/screen-width]) ;; will be an int\n(rf/subscribe [:breaking-point.core/screen-height]) ;; will be an int\n(rf/subscribe [:breaking-point.core/screen]) ;; will be one of the following: :mobile, :tablet, :small-monitor, :large-monitor\n\n(rf/subscribe [:breaking-point.core/orientation]) ;; will be either :portrait or :landscape\n(rf/subscribe [:breaking-point.core/landscape?]) ;; true if width is \u003e= height\n(rf/subscribe [:breaking-point.core/portrait?]) ;; true if height \u003e width\n\n;; these will be based on the breakpoint names that you provide\n(rf/subscribe [:breaking-point.core/mobile?]) ;; true if screen-width is \u003c 768\n(rf/subscribe [:breaking-point.core/tablet?]) ;; true if screen-width is \u003e= 768 and \u003c 992\n(rf/subscribe [:breaking-point.core/small-monitor?]) ;; true if window width is \u003e= 992 and \u003c 1200\n(rf/subscribe [:breaking-point.core/large-monitor?]) ;; true if window width is \u003e= 1200\n```\n\n## Error messages\n\nHelpful error messages are important to kee-frame. You should not get stuck because of \"undefined is not a function\". If you make a mistake, kee-frame should make it very clear to you what you did wrong and how you can fix it. If you find pain spots, please post an issue so we can find better solutions.\n\n## React error boundaries\n\nIf you are unfamiliar with error boundaries, you can read the [docs](https://reactjs.org/docs/error-boundaries.html). After reading [this](https://lilac.town/writing/modern-react-in-cljs-error-boundaries/), I decided to include Will's code snippet\nin kee-frame. Usage:\n\n```clojure\n[kee-frame.error/boundary\n  [your-badly-behaving-component-here props]]\n```\n\nThis will print JS errors inside the component on screen instead of breaking the whole rendering tree.\nYou can optionally include your own error-handling component function as the first param.\n\n[Example usage from demo app](https://github.com/ingesolvoll/kee-frame-sample/blob/master/src/cljs/kee_frame_sample/core.cljs#L26)\n\n## Scroll behavior on navigation\nIn a traditional static website, the browser handles the scrolling for you nicely. Meaning that when you navigate back\nand forward, the browser \"remembers\" how far down you scrolled on the last visit. This is convenient for many websites,\nso Kee-frame utilizes a third-party JS lib to get this behavior for a SPA. The only thing you need to do is this in\nyour main namespace:\n\n```clojure\n(:require [kee-frame.scroll])\n```\n\n## Credits\n\nThe implementation of kee-frame is quite simple, building on rock solid libraries and other people's ideas. The main influence is the [Keechma](https://keechma.com/) framework. It is a superb piece of thinking and work, go check it out! Apart from that, the following libraries make kee-frame possible:\n\n* [re-frame](https://github.com/Day8/re-frame) and [reagent](https://reagent-project.github.io/). The world needs to know about these 2 kings of frontend development, and we all need to contribute to their widespread use. This framework is an attempt in that direction.\n* [reitit](https://github.com/metosin/reitit). Simple and easy bidirectional routing, with very little noise in the syntax.\n* [accountant](https://github.com/venantius/accountant). A navigation library that hooks to any routing system. Made my life so much easier when I discovered it.\n* [etaoin](https://github.com/igrishaev/etaoin) and [lein-test-refresh](https://github.com/jakemcc/lein-test-refresh). 2 good examples of how powerful Clojure is. Etaoin makes browser integration testing fun again, while lein-test-refresh provides you with a development flow that no other platform will give you.\n\nThank you!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fingesolvoll%2Fkee-frame","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fingesolvoll%2Fkee-frame","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fingesolvoll%2Fkee-frame/lists"}