rum.cljs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. (ns frontend.rum
  2. "Utility fns for rum"
  3. (:require [cljs-bean.core :as bean]
  4. [clojure.set :as set]
  5. [clojure.string :as string]
  6. [clojure.walk :as w]
  7. [daiquiri.interpreter :as interpreter]
  8. [frontend.hooks :as hooks]
  9. [rum.core :refer [use-state] :as rum]))
  10. ;; copy from https://github.com/priornix/antizer/blob/35ba264cf48b84e6597743e28b3570d8aa473e74/src/antizer/core.cljs
  11. (defn kebab-case->camel-case
  12. "Converts from kebab case to camel case, eg: on-click to onClick"
  13. [input]
  14. (string/replace input #"-([a-z])" (fn [[_ c]] (string/upper-case c))))
  15. (defn map-keys->camel-case
  16. "Stringifys all the keys of a cljs hashmap and converts them
  17. from kebab case to camel case. If :html-props option is specified,
  18. then rename the html properties values to their dom equivalent
  19. before conversion"
  20. [data & {:keys [html-props]}]
  21. (let [convert-to-camel (fn [[key value]]
  22. [(kebab-case->camel-case (name key)) value])]
  23. (w/postwalk (fn [x]
  24. (if (map? x)
  25. (let [new-map (if html-props
  26. (set/rename-keys x {:class :className :for :htmlFor})
  27. x)]
  28. (into {} (map convert-to-camel new-map)))
  29. x))
  30. data)))
  31. ;; TODO: Replace this with rum's built in rum.core/adapt-class
  32. ;; adapted from https://github.com/tonsky/rum/issues/20
  33. (defn adapt-class
  34. ([react-class]
  35. (adapt-class react-class false))
  36. ([react-class skip-opts-transform?]
  37. (fn [& args]
  38. (let [[opts children] (if (map? (first args))
  39. [(first args) (rest args)]
  40. [{} args])
  41. type# (first children)
  42. ;; we have to make sure to check if the children is sequential
  43. ;; as a list can be returned, eg: from a (for)
  44. new-children (if (sequential? type#)
  45. (let [result (interpreter/interpret children)]
  46. (if (sequential? result)
  47. result
  48. [result]))
  49. children)
  50. ;; convert any options key value to a react element, if
  51. ;; a valid html element tag is used, using sablono
  52. vector->react-elems (fn [[key val]]
  53. (if (sequential? val)
  54. [key (interpreter/interpret val)]
  55. [key val]))
  56. new-options (into {}
  57. (if skip-opts-transform?
  58. opts
  59. (map vector->react-elems opts)))]
  60. (apply js/React.createElement react-class
  61. ;; sablono html-to-dom-attrs does not work for nested hashmaps
  62. (bean/->js (map-keys->camel-case new-options :html-props true))
  63. new-children)))))
  64. (defn use-atom-fn
  65. [a getter-fn setter-fn]
  66. (let [[val set-val] (use-state (getter-fn @a))]
  67. (hooks/use-effect!
  68. (fn []
  69. (let [id (str (random-uuid))]
  70. (add-watch a id (fn [_ _ prev-state next-state]
  71. (let [prev-value (getter-fn prev-state)
  72. next-value (getter-fn next-state)]
  73. (when-not (= prev-value next-value)
  74. (set-val next-value)))))
  75. #(remove-watch a id)))
  76. [])
  77. [val #(swap! a setter-fn %)]))
  78. (defn use-atom
  79. "(use-atom my-atom)"
  80. [a]
  81. (use-atom-fn a identity (fn [_ v] v)))
  82. (defn use-atom-in
  83. [a ks]
  84. (let [ks (if (keyword? ks) [ks] ks)]
  85. (use-atom-fn a #(get-in % ks) (fn [a' v] (assoc-in a' ks v)))))
  86. (defn use-mounted
  87. []
  88. (let [*mounted (rum/use-ref false)]
  89. (hooks/use-effect!
  90. (fn []
  91. (rum/set-ref! *mounted true)
  92. #(rum/set-ref! *mounted false))
  93. [])
  94. #(rum/deref *mounted)))
  95. (defn use-bounding-client-rect
  96. "Returns the bounding client rect for a given dom node
  97. You can manually change the tick value, if you want to force refresh the value, you can manually change the tick value"
  98. ([] (use-bounding-client-rect nil))
  99. ([tick]
  100. (let [[ref set-ref] (rum/use-state nil)
  101. [rect set-rect] (rum/use-state nil)]
  102. (hooks/use-effect!
  103. (if ref
  104. (fn []
  105. (let [update-rect #(set-rect (. ref getBoundingClientRect))
  106. updator (fn [entries]
  107. (when (.-contentRect (first (js->clj entries))) (update-rect)))
  108. observer (js/ResizeObserver. updator)]
  109. (update-rect)
  110. (.observe observer ref)
  111. #(.disconnect observer)))
  112. #())
  113. [ref tick])
  114. [set-ref rect])))
  115. (defn ->breakpoint
  116. "Converts a number to a breakpoint string
  117. Values come from https://tailwindcss.com/docs/responsive-design"
  118. [size]
  119. (cond
  120. (nil? size) :md
  121. (<= size 640) :sm
  122. (<= size 768) :md
  123. (<= size 1024) :lg
  124. (<= size 1280) :xl
  125. (<= size 1536) :xl
  126. :else :2xl))
  127. (defn use-breakpoint
  128. "Returns the current breakpoint
  129. You can manually change the tick value, if you want to force refresh the value, you can manually change the tick value"
  130. ([] (use-breakpoint nil))
  131. ([tick]
  132. (let [[ref rect] (use-bounding-client-rect tick)
  133. bp (->breakpoint (when (some? rect) (.-width rect)))]
  134. [ref bp])))
  135. (defonce *key->atom (atom {}))
  136. (defn cached-derived-atom
  137. "Make sure to return the same atom if `key` is the same."
  138. [ref key f]
  139. (or (get @*key->atom key)
  140. (let [a (rum/derived-atom [ref] key f)]
  141. (swap! *key->atom assoc key a)
  142. a)))