From 1c456acaae07a604471a80cb25bdd1b981d02000 Mon Sep 17 00:00:00 2001 From: Daniel Compton Date: Mon, 13 Nov 2017 16:22:36 +1300 Subject: [PATCH] Add a few re-com utilities --- src/day8/re_frame/trace/utils/re_com.clj | 33 ++++ src/day8/re_frame/trace/utils/re_com.cljs | 222 ++++++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 src/day8/re_frame/trace/utils/re_com.clj create mode 100644 src/day8/re_frame/trace/utils/re_com.cljs diff --git a/src/day8/re_frame/trace/utils/re_com.clj b/src/day8/re_frame/trace/utils/re_com.clj new file mode 100644 index 0000000..841cb15 --- /dev/null +++ b/src/day8/re_frame/trace/utils/re_com.clj @@ -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 diff --git a/src/day8/re_frame/trace/utils/re_com.cljs b/src/day8/re_frame/trace/utils/re_com.cljs new file mode 100644 index 0000000..b3c6390 --- /dev/null +++ b/src/day8/re_frame/trace/utils/re_com.cljs @@ -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))