Code Monkey home page Code Monkey logo

fulcro-cookbook's People

Contributors

brancusi avatar holyjak avatar realgenekim avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

fulcro-cookbook's Issues

Recipe: drag-n-drop system for lists of files

Uses a js npm library to do most of the work, so this is really just an “integration” demo.

(ns app.ui.sortable
  (:require
    [com.fulcrologic.fulcro.algorithms.react-interop :as op]
    ["@dnd-kit/core" :refer [DndContext closestCenter PointerSensor KeyboardSensor useSensor useSensors]]
    ["@dnd-kit/sortable" :refer [arrayMove SortableContext sortableKeyboardCoordinates verticalListSortingStrategy useSortable]]
    ["@dnd-kit/utilities" :refer [CSS]]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :refer [div]]
    [taoensso.timbre :as log]))

(defn use-sortable [options] (js->clj
                               (useSortable (clj->js options))
                               :keywordize-keys true))
(defn use-sensor
  "Wrapper for useSensor"
  ([s] (useSensor s))
  ([s options] (useSensor s (clj->js options))))
(defn use-sensors "Wrapper for useSensors:"
  [& sensors] (apply useSensors sensors))
(def ui-dnd-context (op/react-factory DndContext))
(def ui-sortable-context (op/react-factory SortableContext))

(defsc SortableItem [this {:keys [id label render-item] :as props}]
  {:use-hooks? true}
  (when (and goog.DEBUG (not (string? id)))
    (log/warn "Sortable Item requires a STRING id. You passed: " id ", a " (type id)))
  (let [{:keys [attributes listeners setNodeRef transform transition]} (use-sortable {:id id})
        css-style {:transform  (.toString (.-Transform CSS) (clj->js transform))
                   :transition transition}
        props     (merge
                    attributes
                    listeners
                    {:style css-style
                     :ref   setNodeRef})]
    ((or render-item div) props label)))

(def ui-sortable-item (comp/factory SortableItem {:keyfn :id}))

(defsc SortableList [this {:keys [get-item-order get-item-label get-item-id items onSort render-item]}]
  {:use-hooks? true}
  (let [items        (vec (sort-by get-item-order items))
        item-ids     (clj->js (mapv get-item-id items))
        sensors      (use-sensors
                       (use-sensor PointerSensor)
                       (use-sensor KeyboardSensor {:coordinateGetter sortableKeyboardCoordinates}))
        update-order (fn [^js evt]
                       (let [sid-moved       (.. evt -active -id)
                             sid-over        (.. evt -over -id)
                             old-index       (.indexOf item-ids sid-moved)
                             new-index       (.indexOf item-ids sid-over)
                             sorted-item-ids (arrayMove item-ids old-index new-index)
                             id->item        (zipmap (map get-item-id items) items)
                             sorted-items (mapv id->item sorted-item-ids)]
                         (when onSort (onSort sorted-items))))]
    (ui-dnd-context {:sensors            sensors
                     :collisionDetection closestCenter
                     :onDragEnd          update-order}
      (ui-sortable-context {:strategy verticalListSortingStrategy
                            :items    item-ids}
        (mapv
          (fn [item]
            (ui-sortable-item {:id          (get-item-id item)
                               :render-item render-item
                               :label       (get-item-label item)}))
          items)))))

(def ui-sortable-list
  "[props]

   Render the DnD context, sortable context, and sortable items for dnd-kit. Does not wrap with any kind of list, so you
   should do that externally. You can optionally specify `:render-item` to control the rendering of the specific item, but
   you must be sure to include the `element-props` (which include CSS style for moving the element) in whatever top-level
   dom element you return.

   Props:

   * :get-item-order - A (fn [item] sort-index) for the order of the item
   * :get-item-label - A (fn [item] string-label) to label the item.
   * :get-item-id    - A (fn [item] string-id) to identify the item. You must convert the item id to a string
   * :render-item    - (optional) A (fn [element-props label] react-element) to use to render the item. A div is used by default.
   * :onSort         - A (fn [sorted-items]) that can side-effect to update the data of items so that they are properly sorted.
                       These will be the actual items passed in :items, but in their new preferred order.
   * :items          - The list of items.
  "
  (comp/factory SortableList))

Recipe: custom remote to offload work to a WebWorker

Demonstrate how to easily leverage a custom remote to send work for processing in a Web Worker...

Notes:

  • The Worker registers for message events, and then sends 0 or more progress events, and finally some kind of finish event.
  • The remote supports abort, so a worker can be killed
  • You have to talk via js-serializable messages but the remote can convert to/from EDN so that auto-merge facilities work, thus returning works

Remote

(defn web-worker-remote
  "Create a Fulcro remote that forwards (only) mutations web workers. The web worker
   is given by `js-file`.
   The worker will receive an event of the form: #js {:cmd (name mutation-symbol) :payload (clj->js params) :requestId unique-str}.
   The web worker MUST post a messages that take the form: #js {:cmd 'xxx' :requestId unique-str-from-event :payload {}}`,
   where the payload must always be a map, and the requestId must be the one it is responding to (what it was sent as an event to
   process). The special command `done` means the worker has finished the given mutation, and the `payload` is the return value
   of the mutation. ALL other commands are sent as UPDATES to the same mutation (e.g. progress reports).
   The returned map from the worker will be turned to CLJ, and will be available for normalization via the normal mutation
   `returning` mechanism. It is therefore recommended that you include a namespaced ID in the map for this
   purpose (e.g. #js {'project/id': id-to-normalize ...})."
  [js-file]
  (log/debug "Starting web worker remote" js-file)
  (let [active-requests               (atom {})]
    {:transmit! (fn transmit! [_ {:keys [::txn/ast ::txn/result-handler ::txn/update-handler] :as send-node}]
                  (let [{:keys [dispatch-key params type] :as _mutation-node} (if (= :root (:type ast))
                                                                                (first (:children ast))
                                                                                ast)
                        request-id (str (random-uuid))
                        abort-id (or
                                   (-> send-node ::txn/options ::txn/abort-id)
                                   (-> send-node ::txn/options :abort-id)
                                   request-id)
                        edn (eql/ast->query ast)]
                    (let [worker (js/Worker. js-file)]
                      (try
                        (when (not= type :call) (throw (ex-info "This remote only handles mutations" {})))
                        (let [msg #js {:cmd (name dispatch-key)
                                       :requestId request-id
                                       :payload (clj->js params)}
                              listener (fn listener* [event]
                                         (let [{:keys [cmd payload]} (js->clj (.-data event))]
                                           (log/debug "Received worker event with " cmd payload)
                                           (if (= cmd "done")
                                             (do
                                               (.removeEventListener worker "message" listener*)
                                               (.terminate worker)
                                               (swap! active-requests dissoc request-id)
                                               (try
                                                 (result-handler {:status-code 200
                                                                  :transaction edn
                                                                  :body {dispatch-key payload}})
                                                 (catch :default e
                                                   (log/error e "Result handler for web worker remote failed."))))
                                             (try
                                               (update-handler {:transaction edn
                                                                :body {dispatch-key payload}})
                                               (catch :default e
                                                 (log/error e "Web worker update handler threw unexpected exception."))))))]
                          (swap! active-requests assoc request-id {:listener listener
                                                                   :abort-id abort-id
                                                                   :worker worker})
                          (.addEventListener worker "message" listener)
                          (.postMessage worker msg)
                          request-id)
                        (catch :default e
                          (log/error e "Unexpected internal exception")
                          (result-handler {:status-code 500
                                           :status-text "Internal Error"})
                          (.terminate worker))))))
     :abort! (fn [_ id]
               (when-let [{:keys [worker
                                  listener
                                  request-id]} (reduce-kv
                                                 (fn [_ k v]
                                                   (when (= id (:abort-id v))
                                                     (reduced (assoc v :requestId k))))
                                                 nil
                                                 @active-requests)]
                 (log/debug "Aborting request " request-id)
                 (.removeEventListener worker "message" listener)
                 (.terminate worker)
                 (swap! active-requests dissoc request-id)))}))

Notes: Here we start a new worker for each request. That way, you could also leverage comp/transact!'s option :parallel? true to submit multiple work items in parallel. Depending on your use case, you might want to have a single, reused worker or a pool of workers.

Worker

function progress(request, id, stage) {
  self.postMessage({
    request, payload: {
      "worker-result/status": stage,
      "worker-result/id": id
    }
  })
}

function done(request, output) {
  self.postMessage({
    cmd: "done",
    request, payload: {
      "worker-result/status": "success",
      "worker-result/output": output,
    }
  })
}

function failed(request, error) {
  self.postMessage({
    cmd: "done",
    request, payload: {
      "worker-result/status": "failed",
      "worker-result/output": { errors: error }
    }
  })
}

self.addEventListener('message', (e) => {
  const { data: { cmd, requestId, payload } } = e
  const { text } = payload
  const doTheWork = (text) => { try { done(requestId, text.upperCase()); } catch (e) { failed(requestId, e); } }

  progress(requestId, `Starting...`);

  if (cmd === "transform") {
    setTimeout(() => progress(requestId, `Still working...`), 1000);
    setTimeout(doTheWork, 1000);
  } else { failed(requestId, `Unknown command: ${cmd}`); }
})

UI and mutation

(ns ex-worker-remote
  (:require
    [com.fulcrologic.fulcro.algorithms.merge :as merge]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.mutations :as m]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.algorithms.normalized-state :as fns]
    [com.fulcrologic.fulcro.algorithms.tx-processing :as txn]))

(defsc WorkerResult [_ {:worker-result/keys [status output]}]
  {:query [:worker-result/id :worker-result/output :worker-result/status]
   :ident :worker-result/id}
  (dom/div
    (str "Result status: " (or status "none."))
    (if (= status "failed")
      (dom/div :.ui.red.message (dom/p "Something went wrong:" (pr-str (some-> output (js/JSON.parse) (js->clj :keywordize-keys true) :errors seq))))
      (dom/div "Output: " (dom/kbd output)))))

(def ui-worker-result (comp/factory WorkerResult))

(m/defmutation do-in-worker
  "Send work to a Web Worker & return the result. Input:

    id - The ID of the request. You make it up (SHA of source is recommended).
    input - the input (text) to process
    result-key - (optional) Where the build result should be joined on `ref`.

    The mutation returns a WorkerResult (normalized as `[:worker-result/id id]`).
    "
  [{:keys [id input result-key] :or {result-key ::result}}]
  (action [{:keys [state ref] :as _env}]
          (fns/swap!-> state
                       (assoc-in (conj ref :ui/working?) true)
                       (merge/merge-component WorkerResult {:worker-result/id id
                                                           :worker-result/output {}
                                                           :worker-result/status "working"}
                                              :replace (conj ref result-key))))
  (progress-action [{:keys [state progress] :as _env}]
                   (when-let [status (-> progress :body (get `do-in-worker) :worker-result/status)]
                     (swap! state assoc-in [:worker-result/id id :worker-result/status] status)))
  (ok-action [{:keys [state ref] ::txn/keys [options]}]
             (fns/swap!-> state (assoc-in (conj ref :ui/working?) false))
             (let [status (get-in @state [:worker-result/id id :worker-result/status])
                   {:keys [on-success]} options]
               (when (and (= "success" status) on-success) (on-success))))
  (error-action [{:keys [state ref]}]
                (fns/swap!-> state
                             (assoc-in (conj ref :ui/working?) false)
                             (assoc-in [:worker-result/id id :worker-result/status] "failed")))
  (web-worker [env]
              (-> env
                  (m/with-params {:id id :text input})
                  (m/returning WorkerResult))))

Putting it all together

Create fuclro-app with remotes containing :web-worker (web-worker-remote "path/to/my-worker.js"), add a UI button to transact the do-in-worker mutation and display the progress/result.

Recipe: File processing

The basic idea is you can drop a list of files or a directory. It scans it recursively for every file then computes a SHA for each file, and sends through through a pipeline (see drop-processor). If the code decides the file “matches”, then it can upload it…but the upload is designed to use AWS s3 presigned URLs (to avoid file size limits) so there is a server-side resolver that you can ask (via an ident) if a specific SHA already exists. It responds with exists, along with a presigned URL if it doesn’t thus the upload can short-circuit if that SHA is already in the store.

Here are the basics for file processing:

https://gist.github.com/awkay/3cf6d550986d4e25feefdebb8ff00671

The server-side resolver looks basically like this:

(pcm/defresolver signed-url-resolver
  "Get a signed URL for download/upload of the given file SHA. Requires a logged in user."
  [env {:file-store/keys [sha] :as input}]
  {::pc/input  #{:file-store/sha}
   ::pc/output [:file-store/signed-url
                :file-store/over-quota?
                :file-store/exists?]
   :check      logged-in}
  (let [{:keys [filename]} (:query-params env)
        storage-in-use 0                                    ; TODO: get current user usage...
        over-quota?    (> storage-in-use LIMIT)]
    (if (and sha (> (count sha) 12))
      (let [exists? (and sha (s3/object-exists? sha))]
        (cond-> {:file-store/sha sha :file-store/exists? exists?}
          over-quota? (assoc :file-store/over-quota? true)
          (not over-quota?) (assoc :file-store/exists? exists?)
          (and (not over-quota?) (not exists?)) (assoc :file-store/signed-url
                                                  (.toString (s3/put-url sha)))))
      (log/error "Invalid or missing SHA in upload URL request"))))

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.