Add a few re-com utilities

This commit is contained in:
Daniel Compton 2017-11-13 16:22:36 +13:00
parent 8727b556f8
commit 1c456acaae
2 changed files with 255 additions and 0 deletions

View File

@ -0,0 +1,33 @@
(ns day8.re-frame.trace.utils.re-com)
;; There is a trap when writing DOM event handlers. This looks innocent enough:
;;
;; :on-mouse-out #(reset! my-over-atom false)
;;
;; But notice that it inadvertently returns false. returning false means something!!
;; v0.11 of ReactJS will invoke both stopPropagation() and preventDefault()
;; on the event. Almost certainly not what we want.
;;
;; Note: v0.12 of ReactJS will do the same as v0.11, except it also issues a
;; deprecation warning about false returns.
;;
;; Note: ReactJS only tests explicitly for false, not falsy values. So 'nil' is a
;; safe return value.
;;
;; So 'handler-fn' is a macro which will stop you from inadvertently returning
;; false in a handler.
;;
;;
;; Examples:
;;
;; :on-mouse-out (handler-fn (reset! my-over-atom false)) ;; notice no # in front reset! form
;;
;;
;; :on-mouse-out (handler-fn
;; (reset! over-atom false) ;; notice: no need for a 'do'
;; (now do something else)
;; (.preventDefault event)) ;; notice access to the 'event'
(defmacro handler-fn
([& body]
`(fn [~'event] ~@body nil))) ;; force return nil

View File

@ -0,0 +1,222 @@
(ns day8.re-frame.trace.utils.re-com
"Shameless pilfered from re-com."
(:require-macros [day8.re-frame.trace.utils.re-com :refer [handler-fn]])
(:require [reagent.ratom :as reagent :refer [RAtom Reaction RCursor Track Wrapper]]
[clojure.string :as string]))
(defn deref-or-value
"Takes a value or an atom
If it's a value, returns it
If it's a Reagent object that supports IDeref, returns the value inside it by derefing
"
[val-or-atom]
(if (satisfies? IDeref val-or-atom)
@val-or-atom
val-or-atom))
(defn flex-flow-style
"A cross-browser helper function to output flex-flow with all it's potential browser prefixes"
[flex-flow]
{:-webkit-flex-flow flex-flow
:flex-flow flex-flow})
(defn flex-child-style
"Determines the value for the 'flex' attribute (which has grow, shrink and basis), based on the :size parameter.
IMPORTANT: The term 'size' means width of the item in the case of flex-direction 'row' OR height of the item in the case of flex-direction 'column'.
Flex property explanation:
- grow Integer ratio (used with other siblings) to determined how a flex item grows it's size if there is extra space to distribute. 0 for no growing.
- shrink Integer ratio (used with other siblings) to determined how a flex item shrinks it's size if space needs to be removed. 0 for no shrinking.
- basis Initial size (width, actually) of item before any growing or shrinking. Can be any size value, e.g. 60%, 100px, auto
Note: auto will cause the initial size to be calculated to take up as much space as possible, in conjunction with it's siblings :flex settings.
Supported values:
- initial '0 1 auto' - Use item's width/height for dimensions (or content dimensions if w/h not specifed). Never grow. Shrink (to min-size) if necessary.
Good for creating boxes with fixed maximum size, but that can shrink to a fixed smaller size (min-width/height) if space becomes tight.
NOTE: When using initial, you should also set a width/height value (depending on flex-direction) to specify it's default size
and an optional min-width/height value to specify the size it can shrink to.
- auto '1 1 auto' - Use item's width/height for dimensions. Grow if necessary. Shrink (to min-size) if necessary.
Good for creating really flexible boxes that will gobble as much available space as they are allowed or shrink as much as they are forced to.
- none '0 0 auto' - Use item's width/height for dimensions (or content dimensions if not specifed). Never grow. Never shrink.
Good for creating rigid boxes that stick to their width/height if specified, otherwise their content size.
- 100px '0 0 100px' - Non flexible 100px size (in the flex direction) box.
Good for fixed headers/footers and side bars of an exact size.
- 60% '60 1 0px' - Set the item's size (it's width/height depending on flex-direction) to be 60% of the parent container's width/height.
NOTE: If you use this, then all siblings with percentage values must add up to 100%.
- 60 '60 1 0px' - Same as percentage above.
- grow shrink basis 'grow shrink basis' - If none of the above common valaues above meet your needs, this gives you precise control.
If number of words is not 1 or 3, an exception is thrown.
Reference: http://www.w3.org/TR/css3-flexbox/#flexibility
Diagram: http://www.w3.org/TR/css3-flexbox/#flex-container
Regex101 testing: ^(initial|auto|none)|(\\d+)(px|%|em)|(\\d+)\\w(\\d+)\\w(.*) - remove double backslashes"
[size]
;; TODO: Could make initial/auto/none into keywords???
(let [split-size (string/split (string/trim size) #"\s+") ;; Split into words separated by whitespace
split-count (count split-size)
_ (assert (contains? #{1 3} split-count) "Must pass either 1 or 3 words to flex-child-style")
size-only (when (= split-count 1) (first split-size)) ;; Contains value when only one word passed (e.g. auto, 60px)
split-size-only (when size-only (string/split size-only #"(\d+)(.*)")) ;; Split into number + string
[_ num units] (when size-only split-size-only) ;; grab number and units
pass-through? (nil? num) ;; If we can't split, then we'll pass this straign through
grow-ratio? (or (= units "%") (= units "") (nil? units)) ;; Determine case for using grow ratio
grow (if grow-ratio? num "0") ;; Set grow based on percent or integer, otherwise no grow
shrink (if grow-ratio? "1" "0") ;; If grow set, then set shrink to even shrinkage as well
basis (if grow-ratio? "0px" size) ;; If grow set, then even growing, otherwise set basis size to the passed in size (e.g. 100px, 5em)
flex (if (and size-only (not pass-through?))
(str grow " " shrink " " basis)
size)]
{:-webkit-flex flex
:flex flex}))
(defn justify-style
"Determines the value for the flex 'justify-content' attribute.
This parameter determines how children are aligned along the main axis.
The justify parameter is a keyword.
Reference: http://www.w3.org/TR/css3-flexbox/#justify-content-property"
[justify]
(let [js (case justify
:start "flex-start"
:end "flex-end"
:center "center"
:between "space-between"
:around "space-around")]
{:-webkit-justify-content js
:justify-content js}))
(defn align-style
"Determines the value for the flex align type attributes.
This parameter determines how children are aligned on the cross axis.
The justify parameter is a keyword.
Reference: http://www.w3.org/TR/css3-flexbox/#align-items-property"
[attribute align]
(let [attribute-wk (->> attribute name (str "-webkit-") keyword)
as (case align
:start "flex-start"
:end "flex-end"
:center "center"
:baseline "baseline"
:stretch "stretch")]
{attribute-wk as
attribute as}))
(defn gap-f
"Returns a component which produces a gap between children in a v-box/h-box along the main axis"
[& {:keys [size width height class style attr]
:as args}]
(let [s (merge
(when size (flex-child-style size))
(when width {:width width})
(when height {:height height})
style)]
[:div
(merge
{:class (str "rc-gap " class) :style s}
attr)]))
(defn h-box
"Returns hiccup which produces a horizontal box.
It's primary role is to act as a container for components and lays it's children from left to right.
By default, it also acts as a child under it's parent"
[& {:keys [size width height min-width min-height max-width max-height justify align align-self margin padding gap children class style attr]
:or {size "none" justify :start align :stretch}
:as args}]
(let [s (merge
(flex-flow-style "row nowrap")
(flex-child-style size)
(when width {:width width})
(when height {:height height})
(when min-width {:min-width min-width})
(when min-height {:min-height min-height})
(when max-width {:max-width max-width})
(when max-height {:max-height max-height})
(justify-style justify)
(align-style :align-items align)
(when align-self (align-style :align-self align-self))
(when margin {:margin margin}) ;; margin and padding: "all" OR "top&bottom right&left" OR "top right bottom left"
(when padding {:padding padding})
style)
gap-form (when gap [gap-f
:size gap
:width gap]) ;; TODO: required to get around a Chrome bug: https://code.google.com/p/chromium/issues/detail?id=423112. Remove once fixed.
children (if gap
(interpose gap-form (filter identity children)) ;; filter is to remove possible nils so we don't add unwanted gaps
children)]
(into [:div
(merge
{:class (str "rc-h-box display-flex " class) :style s}
attr)]
children)))
(defn- input-text-base
"Returns markup for a basic text input label"
[& {:keys [model input-type] :as args}]
(let [external-model (reagent/atom (deref-or-value model)) ;; Holds the last known external value of model, to detect external model changes
internal-model (reagent/atom (if (nil? @external-model) "" @external-model))] ;; Create a new atom from the model to be used internally (avoid nil)
(fn
[& {:keys [model on-change status status-icon? status-tooltip placeholder width height rows change-on-blur? validation-regex disabled? class style attr]
:or {change-on-blur? true}
:as args}]
(let [latest-ext-model (deref-or-value model)
disabled? (deref-or-value disabled?)
change-on-blur? (deref-or-value change-on-blur?)
showing? (reagent/atom false)]
(when (not= @external-model latest-ext-model) ;; Has model changed externally?
(reset! external-model latest-ext-model)
(reset! internal-model latest-ext-model))
[h-box
:class "rc-input-text "
:align :start
:width (if width width "250px")
:children [[:div
{:class (str "rc-input-text-inner " ;; form-group
(case status
:success "has-success "
:warning "has-warning "
:error "has-error "
"")
(when (and status status-icon?) "has-feedback"))
:style (flex-child-style "auto")}
[(if (= input-type :password) :input input-type)
(merge
{:class (str "form-control " class)
:type (case input-type
:input "text"
:password "password"
nil)
:rows (when (= input-type :textarea) (or rows 3))
:style (merge
(flex-child-style "none")
{:height height
:padding-right "12px"} ;; override for when icon exists
style)
:placeholder placeholder
:value @internal-model
:disabled disabled?
:on-change (handler-fn
(let [new-val (-> event .-target .-value)]
(when (and
on-change
(not disabled?)
(if validation-regex (re-find validation-regex new-val) true))
(reset! internal-model new-val)
(when-not change-on-blur?
(on-change @internal-model)))))
:on-blur (handler-fn
(when (and
on-change
change-on-blur?
(not= @internal-model @external-model))
(on-change @internal-model)))
:on-key-up (handler-fn
(if disabled?
(.preventDefault event)
(case (.-which event)
13 (when on-change (on-change @internal-model))
27 (reset! internal-model @external-model)
true)))}
attr)]]]]))))
(defn input-text
[& args]
(apply input-text-base :input-type :input args))