From ab0245279f4e85036e7cb7b4591921320fc819a5 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 6 Jul 2023 15:27:14 +0200 Subject: [PATCH] :recycle: Refactor (again) numeric input component --- .../app/main/ui/components/color_input.cljs | 16 +- .../app/main/ui/components/numeric_input.cljs | 271 ++++++++---------- .../workspace/sidebar/options/menus/fill.cljs | 2 +- .../sidebar/options/menus/stroke.cljs | 2 +- .../sidebar/options/menus/typography.cljs | 4 +- .../ui/workspace/sidebar/options/page.cljs | 38 +-- .../sidebar/options/rows/color_row.cljs | 4 +- .../sidebar/options/rows/input_row.cljs | 5 +- .../sidebar/options/rows/stroke_row.cljs | 6 +- frontend/src/app/util/dom.cljs | 27 +- frontend/src/app/util/object.cljs | 5 + 11 files changed, 182 insertions(+), 198 deletions(-) diff --git a/frontend/src/app/main/ui/components/color_input.cljs b/frontend/src/app/main/ui/components/color_input.cljs index 2882ce8da..be8555e68 100644 --- a/frontend/src/app/main/ui/components/color_input.cljs +++ b/frontend/src/app/main/ui/components/color_input.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.components.color-input (:require + [app.common.data :as d] [app.util.color :as uc] [app.util.dom :as dom] [app.util.globals :as globals] @@ -13,8 +14,7 @@ [app.util.keyboard :as kbd] [app.util.object :as obj] [goog.events :as events] - [rumext.v2 :as mf]) - (:import goog.events.EventType)) + [rumext.v2 :as mf])) (defn clean-color [value] @@ -31,7 +31,7 @@ on-change (obj/get props "onChange") on-blur (obj/get props "onBlur") on-focus (obj/get props "onFocus") - select-on-focus? (obj/get props "data-select-on-focus" true) + select-on-focus? (d/nilv (unchecked-get props "selectOnFocus") true) ;; We need a ref pointing to the input dom element, but the user ;; of this component may provide one (that is forwarded here). @@ -128,8 +128,10 @@ ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect (.addEventListener target "mouseup" on-mouse-up #js {"once" true}))))) - props (-> props - (obj/without ["value" "onChange" "onFocus"]) + props (-> (obj/clone props) + (obj/unset! "selectOnFocus") + (obj/set! "value" mf/undefined) + (obj/set! "onChange" mf/undefined) (obj/set! "type" "text") (obj/set! "ref" ref) ;; (obj/set! "list" list-id) @@ -157,8 +159,8 @@ (mf/use-layout-effect (fn [] - (let [keys [(events/listen globals/window EventType.POINTERDOWN on-click) - (events/listen globals/window EventType.CLICK on-click)]] + (let [keys [(events/listen globals/window "pointerdown" on-click) + (events/listen globals/window "click" on-click)]] #(doseq [key keys] (events/unlistenByKey key))))) diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs index a7d3ed106..e9acffbde 100644 --- a/frontend/src/app/main/ui/components/numeric_input.cljs +++ b/frontend/src/app/main/ui/components/numeric_input.cljs @@ -7,153 +7,135 @@ (ns app.main.ui.components.numeric-input (:require [app.common.data :as d] - [app.common.spec :as us] + [app.common.schema :as sm] [app.main.ui.formats :as fmt] + [app.main.ui.hooks :as h] [app.util.dom :as dom] [app.util.globals :as globals] [app.util.keyboard :as kbd] [app.util.object :as obj] - [app.util.simple-math :as sm] + [app.util.simple-math :as smt] + [cljs.core :as c] [cuerdas.core :as str] [goog.events :as events] - [rumext.v2 :as mf]) - (:import goog.events.EventType)) + [rumext.v2 :as mf])) (mf/defc numeric-input {::mf/wrap-props false ::mf/forward-ref true} [props external-ref] - (let [value-str (obj/get props "value") - min-val-str (obj/get props "min") - max-val-str (obj/get props "max") - step-val-str (obj/get props "step") - wrap-value? (obj/get props "data-wrap") - on-change (obj/get props "onChange") - on-blur (obj/get props "onBlur") - on-focus (obj/get props "onFocus") - title (obj/get props "title") - default-val (obj/get props "default") - nillable (obj/get props "nillable") - select-on-focus? (obj/get props "data-select-on-focus" true) - class (obj/get props "klass") + (let [value-str (unchecked-get props "value") + min-value (unchecked-get props "min") + max-value (unchecked-get props "max") + step-value (unchecked-get props "step") + wrap-value? (unchecked-get props "data-wrap") + on-change (unchecked-get props "onChange") + on-blur (unchecked-get props "onBlur") + on-focus (unchecked-get props "onFocus") + + title (unchecked-get props "title") + default (unchecked-get props "default") + nillable? (unchecked-get props "nillable") + class (d/nilv (unchecked-get props "className") "input-text") + + min-value (d/parse-double min-value) + max-value (d/parse-double max-value) + step-value (d/parse-double step-value 1) + default (d/parse-double default 0) + + select-on-focus? (d/nilv (unchecked-get props "selectOnFocus") true) ;; We need a ref pointing to the input dom element, but the user ;; of this component may provide one (that is forwarded here). ;; So we use the external ref if provided, and the local one if not. - local-ref (mf/use-ref) - ref (or external-ref local-ref) - - ;; We need to store the handle-blur ref so we can call it on unmount - handle-blur-ref (mf/use-ref nil) - dirty-ref (mf/use-ref false) + local-ref (mf/use-ref) + ref (or external-ref local-ref) ;; This `value` represents the previous value and is used as ;; initil value for the simple math expression evaluation. - value (d/parse-double value-str default-val) + value (d/parse-double value-str default) - min-val (cond - (number? min-val-str) - min-val-str - - (string? min-val-str) - (d/parse-double min-val-str)) - - max-val (cond - (number? max-val-str) - max-val-str - - (string? max-val-str) - (d/parse-double max-val-str)) - - step-val (cond - (number? step-val-str) - step-val-str - - (string? step-val-str) - (d/parse-double step-val-str) - - :else 1) + ;; We need to store the handle-blur ref so we can call it on unmount + dirty-ref (mf/use-ref false) parse-value - (mf/use-callback - (mf/deps ref min-val max-val value nillable default-val) + (mf/use-fn + (mf/deps min-value max-value value nillable? default) (fn [] - (let [input-node (mf/ref-val ref) - new-value (-> (dom/get-value input-node) - (str/strip-suffix ".") - (sm/expr-eval value))] - (cond - (d/num? new-value) - (-> new-value - (cljs.core/max (/ us/min-safe-int 2)) - (cljs.core/min (/ us/max-safe-int 2)) - (cond-> - (d/num? min-val) - (cljs.core/max min-val) + (when-let [node (mf/ref-val ref)] + (let [new-value (-> (dom/get-value node) + (str/strip-suffix ".") + (smt/expr-eval value))] + (cond + (d/num? new-value) + (-> new-value + (d/max (/ sm/min-safe-int 2)) + (d/min (/ sm/max-safe-int 2)) + (cond-> (d/num? min-value) + (d/max min-value)) + (cond-> (d/num? max-value) + (d/min max-value))) - (d/num? max-val) - (cljs.core/min max-val))) + nillable? + default - nillable - default-val - - :else value)))) + :else value))))) update-input - (mf/use-callback - (mf/deps ref) + (mf/use-fn (fn [new-value] - (let [input-node (mf/ref-val ref)] - (dom/set-value! input-node (fmt/format-number new-value))))) + (when-let [node (mf/ref-val ref)] + (dom/set-value! node (fmt/format-number new-value))))) apply-value - (mf/use-callback + (mf/use-fn (mf/deps on-change update-input value) - (fn [new-value event] + (fn [event new-value] (mf/set-ref-val! dirty-ref false) (when (and (not= new-value value) (fn? on-change)) + ;; FIXME: on-change very slow, makes the handler laggy (on-change new-value event)) (update-input new-value))) set-delta - (mf/use-callback - (mf/deps wrap-value? min-val max-val parse-value apply-value) + (mf/use-fn + (mf/deps wrap-value? min-value max-value parse-value apply-value) (fn [event up? down?] (let [current-value (parse-value)] (when current-value (let [increment (cond (kbd/shift? event) - (if up? (* step-val 10) (* step-val -10)) + (if up? (* step-value 10) (* step-value -10)) (kbd/alt? event) - (if up? (* step-val 0.1) (* step-val -0.1)) + (if up? (* step-value 0.1) (* step-value -0.1)) :else - (if up? step-val (- step-val))) + (if up? step-value (- step-value))) new-value (+ current-value increment) new-value (cond - (and wrap-value? (d/num? max-val min-val) - (> new-value max-val) up?) - (-> new-value (- max-val) (+ min-val) (- step-val)) + (and wrap-value? (d/num? max-value min-value) + (> new-value max-value) up?) + (-> new-value (- max-value) (+ min-value) (- step-value)) - (and wrap-value? (d/num? max-val min-val) - (< new-value min-val) down?) - (-> new-value (- min-val) (+ max-val) (+ step-val)) + (and wrap-value? (d/num? max-value min-value) + (< new-value min-value) down?) + (-> new-value (- min-value) (+ max-value) (+ step-value)) - (and (d/num? min-val) (< new-value min-val)) - min-val + (and (d/num? min-value) (< new-value min-value)) + min-value - (and (d/num? max-val) (> new-value max-val)) - max-val + (and (d/num? max-value) (> new-value max-value)) + max-value :else new-value)] - (apply-value new-value event)))))) + (apply-value event new-value)))))) handle-key-down - (mf/use-callback + (mf/use-fn (mf/deps set-delta apply-value update-input) (fn [event] (mf/set-ref-val! dirty-ref true) @@ -161,44 +143,47 @@ down? (kbd/down-arrow? event) enter? (kbd/enter? event) esc? (kbd/esc? event) - input-node (mf/ref-val ref)] + node (mf/ref-val ref)] (when (or up? down?) (set-delta event up? down?)) (when enter? - (dom/blur! input-node)) + (dom/blur! node)) (when esc? (update-input value-str) - (dom/blur! input-node))))) + (dom/blur! node))))) handle-mouse-wheel - (mf/use-callback + (mf/use-fn (mf/deps set-delta) (fn [event] - (let [input-node (mf/ref-val ref)] - (when (dom/active? input-node) - (let [event (.getBrowserEvent ^js event)] - (dom/prevent-default event) - (dom/stop-propagation event) - (set-delta event (< (.-deltaY event) 0) (> (.-deltaY event) 0))))))) + (when-let [node (mf/ref-val ref)] + (when (dom/active? node) + (dom/prevent-default event) + (dom/stop-propagation event) + (let [{:keys [y]} (dom/get-delta-position event)] + (set-delta event (< y 0) (> y 0))))))) handle-blur - (mf/use-callback + (mf/use-fn (mf/deps parse-value apply-value update-input on-blur) (fn [event] - (let [new-value (or (parse-value) default-val)] - (if (or nillable new-value) - (apply-value new-value event) + (let [new-value (or (parse-value) default)] + (if (or nillable? new-value) + (apply-value event new-value) (update-input new-value))) - (when on-blur (on-blur event)))) + (when (fn? on-blur) + (on-blur event)))) + + handle-unmount + (h/use-ref-callback handle-blur) on-click - (mf/use-callback + (mf/use-fn (fn [event] - (let [target (dom/get-target event)] - (when (some? ref) - (let [current (mf/ref-val ref)] - (when (and (some? current) (not (.contains current target))) - (dom/blur! current))))))) + (let [target (dom/get-target event) + node (mf/ref-val ref)] + (when (and (some? node) (not (dom/child? node target))) + (dom/blur! node))))) handle-focus (mf/use-callback @@ -212,9 +197,12 @@ ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect (.addEventListener target "mouseup" dom/prevent-default #js {:once true}))))) - props (-> props - (obj/without ["value" "onChange" "nillable" "onFocus"]) - (obj/set! "className" (or class "input-text")) + props (-> (obj/clone props) + (obj/unset! "selectOnFocus") + (obj/unset! "nillable") + (obj/set! "value" mf/undefined) + (obj/set! "onChange" mf/undefined) + (obj/set! "className" class) (obj/set! "type" "text") (obj/set! "ref" ref) (obj/set! "defaultValue" (fmt/format-number value)) @@ -223,50 +211,23 @@ (obj/set! "onBlur" handle-blur) (obj/set! "onFocus" handle-focus))] - (mf/use-effect - (mf/deps value) - (fn [] - (when-let [input-node (mf/ref-val ref)] - (dom/set-value! input-node (fmt/format-number value))))) + (mf/with-effect [value] + (when-let [input-node (mf/ref-val ref)] + (dom/set-value! input-node (fmt/format-number value)))) - (mf/use-effect - (mf/deps handle-blur) - (fn [] - (mf/set-ref-val! handle-blur-ref {:fn handle-blur}))) + (mf/with-effect [] + (fn [] + (when (mf/ref-val dirty-ref) + (handle-unmount)))) - (mf/use-layout-effect - (fn [] - #(when (mf/ref-val dirty-ref) - (let [handle-blur (:fn (mf/ref-val handle-blur-ref))] - (handle-blur))))) + (mf/with-layout-effect [] + (let [keys [(events/listen globals/window "pointerdown" on-click) + (events/listen globals/window "click" on-click)]] + #(run! events/unlistenByKey keys))) - (mf/use-layout-effect - (mf/deps handle-mouse-wheel) - (fn [] - (let [keys [(events/listen (mf/ref-val ref) EventType.WHEEL handle-mouse-wheel #js {:passive false})]] - #(doseq [key keys] - (events/unlistenByKey key))))) - - (mf/use-layout-effect - (fn [] - (let [keys [(events/listen globals/window EventType.POINTERDOWN on-click) - (events/listen globals/window EventType.CLICK on-click)]] - #(doseq [key keys] - (events/unlistenByKey key))))) - - (mf/use-layout-effect - (mf/deps handle-mouse-wheel) - (fn [] - (let [keys [(events/listen (mf/ref-val ref) EventType.WHEEL handle-mouse-wheel #js {:passive false})]] - #(doseq [key keys] - (events/unlistenByKey key))))) - - - (mf/use-layout-effect - (mf/deps handle-mouse-wheel) - (fn [] - (let [keys [(events/listen (mf/ref-val ref) EventType.WHEEL handle-mouse-wheel #js {:passive false})]] - #(doseq [key keys] - (events/unlistenByKey key))))) + (mf/with-layout-effect [handle-mouse-wheel] + (when-let [node (mf/ref-val ref)] + (let [key (events/listen node "wheel" handle-mouse-wheel #js {:passive false})] + #(events/unlistenByKey key)))) [:> :input props])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index dd7eff0e1..94631c9d4 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -151,7 +151,7 @@ :on-remove (on-remove index) :disable-drag disable-drag :on-focus on-focus - :data-select-on-focus (not @disable-drag) + :select-on-focus (not @disable-drag) :on-blur on-blur}])]) (when (or (= type :frame) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index dba1339cc..a00e21a3e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -189,6 +189,6 @@ :on-reorder (handle-reorder index) :disable-drag disable-drag :on-focus on-focus - :data-select-on-focus (not @disable-drag) + :select-on-focus (not @disable-drag) :on-blur on-blur :disable-stroke-style disable-stroke-style}])])]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index bea7507b7..1780103db 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -461,7 +461,7 @@ :max 200 :step 0.1 :default "1.2" - :klass (css :line-height-input) + :class (css :line-height-input) :value (attr->string line-height) :placeholder (tr "settings.multiple") :nillable line-height-nillable @@ -477,7 +477,7 @@ {:min -200 :max 200 :step 0.1 - :klass (css :letter-spacing-input) + :class (css :letter-spacing-input) :value (attr->string letter-spacing) :placeholder (tr "settings.multiple") :on-change #(handle-change % :letter-spacing) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs index dd9341be8..e5a9a92ad 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs @@ -17,31 +17,23 @@ [rumext.v2 :as mf])) (mf/defc options - {::mf/wrap [mf/memo]} + {::mf/wrap [mf/memo] + ::mf/wrap-props false} [] - (let [options (mf/deref refs/workspace-page-options) - - on-change - (fn [value] - (st/emit! (dw/change-canvas-color value))) - - on-open - (fn [] - (st/emit! (dwu/start-undo-transaction :options))) - - on-close - (fn [] - (st/emit! (dwu/commit-undo-transaction :options)))] - + (let [options (mf/deref refs/workspace-page-options) + on-change (mf/use-fn #(st/emit! (dw/change-canvas-color %))) + on-open (mf/use-fn #(st/emit! (dwu/start-undo-transaction :options))) + on-close (mf/use-fn #(st/emit! (dwu/commit-undo-transaction :options)))] [:div.element-set [:div.element-set-title (tr "workspace.options.canvas-background")] [:div.element-set-content - [:& color-row {:disable-gradient true - :disable-opacity true - :title (tr "workspace.options.canvas-background") - :color {:color (get options :background clr/canvas) - :opacity 1} - :on-change on-change - :on-open on-open - :on-close on-close}]]])) + [:& color-row + {:disable-gradient true + :disable-opacity true + :title (tr "workspace.options.canvas-background") + :color {:color (get options :background clr/canvas) + :opacity 1} + :on-change on-change + :on-open on-open + :on-close on-close}]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 4b1cd5ad5..c5317b97c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -41,7 +41,7 @@ (mf/defc color-row [{:keys [index color disable-gradient disable-opacity on-change on-reorder on-detach on-open on-close title on-remove - disable-drag on-focus on-blur select-only data-select-on-focus]}] + disable-drag on-focus on-blur select-only select-on-focus]}] (let [current-file-id (mf/use-ctx ctx/current-file-id) file-colors (mf/deref refs/workspace-file-colors) shared-libs (mf/deref refs/workspace-libraries) @@ -207,7 +207,7 @@ {:class (dom/classnames :percentail (not= (:opacity color) :multiple))} [:> numeric-input {:value (-> color :opacity opacity->string) :placeholder (tr "settings.multiple") - :data-select-on-focus data-select-on-focus + :select-on-focus select-on-focus :on-focus on-focus :on-blur on-blur :on-change handle-opacity-change diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/input_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/input_row.cljs index 4fde23197..bdb6c7d7e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/input_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/input_row.cljs @@ -12,7 +12,8 @@ [app.util.object :as obj] [rumext.v2 :as mf])) -(mf/defc input-row [{:keys [label options value class min max on-change type placeholder default nillable on-focus data-select-on-focus]}] +(mf/defc input-row + [{:keys [label options value class min max on-change type placeholder default nillable on-focus select-on-focus]}] [:div.row-flex.input-row [:span.element-set-subtitle label] [:div.input-element {:class class} @@ -47,7 +48,7 @@ :nillable nillable :on-change on-change :on-focus on-focus - :data-select-on-focus data-select-on-focus + :select-on-focus select-on-focus :value (or value "")}])]]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs index 7f30ce039..3318fd65f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs @@ -49,7 +49,7 @@ (mf/defc stroke-row {::mf/wrap-props false} - [{:keys [index stroke title show-caps on-color-change on-reorder on-color-detach on-remove on-stroke-width-change on-stroke-style-change on-stroke-alignment-change open-caps-select close-caps-select on-stroke-cap-start-change on-stroke-cap-end-change on-stroke-cap-switch disable-drag on-focus on-blur disable-stroke-style data-select-on-focus]}] + [{:keys [index stroke title show-caps on-color-change on-reorder on-color-detach on-remove on-stroke-width-change on-stroke-style-change on-stroke-alignment-change open-caps-select close-caps-select on-stroke-cap-start-change on-stroke-cap-end-change on-stroke-cap-switch disable-drag on-focus on-blur disable-stroke-style select-on-focus]}] (let [start-caps-state (mf/use-state {:open? false :top 0 :left 0}) @@ -88,7 +88,7 @@ :on-remove (on-remove index) :disable-drag disable-drag :on-focus on-focus - :data-select-on-focus data-select-on-focus + :select-on-focus select-on-focus :on-blur on-blur}] ;; Stroke Width, Alignment & Style @@ -103,7 +103,7 @@ :placeholder (tr "settings.multiple") :on-change (on-stroke-width-change index) :on-focus on-focus - :data-select-on-focus data-select-on-focus + :select-on-focus select-on-focus :on-blur on-blur}]] [:select#style.input-select {:value (enum->string (:stroke-alignment stroke)) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index abdb7b8c2..c9babc26d 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -17,7 +17,21 @@ [app.util.webapi :as wapi] [cuerdas.core :as str] [goog.dom :as dom] - [promesa.core :as p])) + [promesa.core :as p]) + (:import goog.events.BrowserEvent)) + +(extend-type BrowserEvent + cljs.core/IDeref + (-deref [it] (.getBrowserEvent it))) + + +(defn browser-event? + [o] + (instance? BrowserEvent o)) + +(defn native-event? + [o] + (instance? js/Event o)) (log/set-level! :warn) @@ -338,10 +352,19 @@ y (.-offsetY event)] (gpt/point x y)))) +(defn get-delta-position + [event] + (let [e (if (browser-event? event) + (deref event) + event) + x (.-deltaX ^js e) + y (.-deltaY ^js e)] + (gpt/point x y))) + (defn get-client-size [^js node] (when (some? node) - (grc/make-rect 0 0 (.-clientWidth ^js node) (.-clientHeight ^js node)))) + (grc/make-rect 0 0 (.-clientWidth node) (.-clientHeight node)))) (defn get-bounding-rect [node] diff --git a/frontend/src/app/util/object.cljs b/frontend/src/app/util/object.cljs index 602041171..e972a2396 100644 --- a/frontend/src/app/util/object.cljs +++ b/frontend/src/app/util/object.cljs @@ -73,6 +73,11 @@ (unchecked-set obj key value) obj) +(defn unset! + [obj key] + (js-delete obj key) + obj) + (defn update! [obj key f & args] (let [found (get obj key ::not-found)]