Fork me on GitHub

looking at fulcro-rad-datomic, how would you solve paging results and how would you solve filtering results based on what the current user can “see”? I am looking to write something similar for another EQL query supporting provider


I’ve written several versions of this…there is the start of this in the lib. and then you just need a modified state machine. There is one that does a “more” style of loading at


but if you’re confident you can get a count of the docs, it’s not hard (I have one locally) to show the full count and pagination control even though you only load a single page.


Main thing is you need to be able to use datoms or index-pull to get index-sorted results so that you can get fast and accurate loads of pages.


In terms of filtering I’ve found an adaptive approach works best: You can use tuples to get results that are sorted and grouped in a particular way, but then you need to do lazy take-while kinds of things (if the filter has frequent hits), I adapt over to using q once the filters have enough constraints and just load all those results at once. So, the state machine has to be aware of that possibility.


So your resolver ends up looking a bit complicated “If it’s this kind of sort/filter do it this way, else this, else that”. And return a page if you can, but return them all once it gets down to a relatively tight constraint (because once you’re using q you’d have to sort/filter them, and at that point might as well push all that to the client).


Here’s the non-released one that can do one page at a time:

(ns app.lib.rad.machines.indexed-report-machine
    [com.fulcrologic.fulcro.components :as comp]
    [com.fulcrologic.fulcro.raw.components :as rc]
    [com.fulcrologic.fulcro.ui-state-machines :as uism :refer [defstatemachine]]
    [com.fulcrologic.rad.attributes :as attr]
    [com.fulcrologic.rad.options-util :refer [?!]]
    [ :as report]
    [ :as ro]
    [ :as dt]
    [taoensso.timbre :as log]))

(defn report-cache-expired?
  "Helper for state machines. Returns true if the report data looks like it has expired according to configured
   caching parameters."
  [{::uism/keys [state-map] :as uism-env}]
  (let [Report              (uism/actor-class uism-env :actor/report)
        {::report/keys [load-cache-seconds
                        load-cache-expired?]} (comp/component-options Report)
        now-ms              (inst-ms (dt/now))
        last-load-time      (uism/retrieve uism-env :last-load-time)
        cache-expiration-ms (* 1000 (or load-cache-seconds 0))
        cache-looks-stale?  (or
                              (nil? last-load-time)
                              (< last-load-time (- now-ms cache-expiration-ms)))]

(def Q (memoize
         (fn [nm query]
           (rc/nc query {:componentName nm}))))

(defn- load-page
  "Load a page of items for the report."
  [{::uism/keys [state-map] :as env} page-number]
  (let [Report     (uism/actor-class env :actor/report)
        all-params (report/current-control-parameters env)
        page-size  (or (rc/component-options Report ro/page-size) 20)
        offset     (* page-size (dec page-number))
        source     (rc/component-options Report ro/source-attribute)
        BodyItem   (rc/component-options Report ro/BodyItem)
        Query      (Q ::Page [:next-offset :total {:results (rc/get-query BodyItem state-map)}])]
    (-> env
      (uism/load source Query
        {:params             (merge all-params {:offset offset
                                                :limit  page-size})
         ::uism/target-alias :loaded-data
         ::uism/ok-event     :event/loaded
         ::uism/ok-data      {:page-number page-number}
         ::uism/error-event  :event/failed})
      (uism/activate :state/loading))))

(defn load-more!
  "Ask a server-paginated report to load more data at the end of the results. "
  [this] (uism/trigger! this (comp/get-ident this) :event/load-more))

(defn clear-report-data [env]
  (uism/assoc-aliased env :current-rows [] :pages {} :loaded-data {} :busy? false :page-count 0 :current-page 1))

(defn goto-page [env page-number]
  (let [pages       (uism/alias-value env :pages)
        page-count  (uism/alias-value env :page-count)
        ;; we have to load page 1 first in order to ensure we get the total number of items
        page-number (if (pos? page-count)
                      (max 1 (min page-count page-number))
        page        (get pages page-number)
        loaded?     (seq page)]
    (if loaded?
      (-> env
        (uism/assoc-aliased :current-page page-number)
        (uism/assoc-aliased :current-rows page))
      (load-page env page-number))))

(defn populate-page-map [env]
  (let [{:keys [page-number]} (::uism/event-data env)
        Report      (uism/actor-class env :actor/report)
        {:keys [loaded-total loaded-page]} (log/spy :debug (uism/aliased-data env))
        page-size   (or (rc/component-options Report ro/page-size) 20)
        page-count  (when loaded-total (inc (int (/ loaded-total page-size))))
        page-number (max 1 (min (log/spy :debug page-count) (log/spy :debug page-number)))
        {::report/keys [row-pk report-loaded]} (comp/component-options Report)
        table-name  (::attr/qualified-key row-pk)
        pages-path  (uism/resolve-alias env :pages)]
    ;; It is possible (when using more than one filter) for the results to exceed the page size. This happens because
    ;; the multi-filter always returns all the results. So, we have to push that into multiple pages when it happens
    (if (and (= 1 page-number) (> (count loaded-page) page-size))
      (let [pages    (partition-all page-size loaded-page)
            page-map (into {} (map (fn [i pgs] [i (vec pgs)]) (range 1 1000) pages))]
        (uism/assoc-aliased env :pages page-map :loaded-total loaded-total :page-count page-count
          :current-page 1 :current-rows (get page-map 1)))
      (as-> env $
        (uism/apply-action $ assoc-in (conj pages-path page-number) (log/spy :debug loaded-page))
        (cond-> $
          (and (= 1 page-number) (pos? loaded-total)) (uism/assoc-aliased
                                                        :loaded-total (log/spy :debug loaded-total)
                                                        :page-count page-count))
        (uism/assoc-aliased $ :current-page page-number :current-rows loaded-page)))))

(defn page-loaded [{::uism/keys [event-data state-map] :as env}]
  (let [{:keys [page-number]} event-data
        Report       (uism/actor-class env :actor/report)
        loaded-total (uism/alias-value env :loaded-total)
        page-size    (or (rc/component-options Report ro/page-size) 20)
        page-count   (inc (int (/ loaded-total page-size)))
        page-number  (max 1 (min (log/spy :debug page-count) (log/spy :debug page-number)))
        {::report/keys [report-loaded]} (comp/component-options Report)]
    (-> env
      (uism/activate :state/gathering-parameters)
        (and (= 1 page-number) report-loaded) report-loaded))))

(defn reload! [env]
  (-> env
    (goto-page 1)))

(defstatemachine indexed-report-machine
  {::uism/actors #{:actor/report}

   {:parameters   [:actor/report :ui/parameters]
    :current-page [:actor/report :ui/parameters ::report/current-page]
    :sort-params  [:actor/report :ui/parameters ::sort]     ; unused, but here to prevent warning from inherited code
    :loaded-data  [:actor/report :ui/loaded-data]
    :next-offset  [:actor/report :ui/loaded-data :next-offset]
    :loaded-total [:actor/report :ui/loaded-data :total]
    :loaded-page  [:actor/report :ui/loaded-data :results]
    :pages        [:actor/report :ui/cache ::report/pages]
    :current-rows [:actor/report :ui/current-rows]
    :page-count   [:actor/report :ui/page-count]
    :busy?        [:actor/report :ui/busy?]}

    {::uism/handler (fn [env]
                      (let [{::uism/keys [fulcro-app event-data]} env]
                        (-> env
                          (uism/store :route-params (:route-params event-data))
                          (goto-page 1))))}

     (merge report/global-events
       {:event/loaded {::uism/handler page-loaded}
        :event/failed {::uism/handler (fn [env] (log/error "Report failed to load.")
                                        (uism/activate env :state/gathering-parameters))}})}

     (merge report/global-events
       {:event/goto-page         {::uism/handler (fn [{::uism/keys [event-data] :as env}]
                                                   (let [{:keys [page]} event-data]
                                                     (goto-page env page)))}
        :event/next-page         {::uism/handler (fn [env]
                                                   (let [page (uism/alias-value env :current-page)]
                                                     (goto-page env (inc page))))}
        :event/prior-page        {::uism/handler (fn [env]
                                                   (let [page (uism/alias-value env :current-page)]
                                                     (goto-page env (dec page))))}

        :event/filter            {::uism/handler reload!}

        :event/set-ui-parameters {::uism/handler report/initialize-parameters}

        :event/run               {::uism/handler reload!}

        :event/resume            {::uism/handler (fn [env]
                                                   (when (report-cache-expired? env)
                                                     (let [env (report/initialize-parameters env)]
                                                       (reload! env))))}})}}})


That one expects you to return :total and :results.


@U0522TWDA feel free to refine it and submit it as an MR 😄

thinking-face 1

It could use some touch-ups to make it lib-ready


merge request


same as PR basically 😛

👍 1