Fork me on GitHub
#fulcro
<
2020-04-02
>
tony.kay02:04:25

OK, holy crap…Fulcro had a great solution to the input problem all along and I just never saw it (really I inherited input rendering from Om Next and never really looked into it that deeply). Fulcro has what I call “props tunneling”. It’s part of the optimized render story where internally we time-stamp the prop tree that is passed to components when we render the tree, and can then send updated props via setState . defsc wraps render and pulls out the “newest” of these two props as props. Well, that means we’re already able to use setState to update the props of the component from anywhere, and if we do that synchronously then we will end up doing exactly what React wants us to do for inputs: call setState during the event handling! So, I added an option called :synchronous? true to transact! (and set it to true in the helpers like m/set-string! ). When that option is enabled and you call transact with a component then it will immediately run the optimistic actions, pull new UI props for the component, and tunnel props to it! It’s seamless!

4
tony.kay02:04:33

now I can use raw DOM input elements in Fulcro components and it is super fast and never causes cursor jumps due to timing!

tony.kay02:04:02

now my problem is: “how do I unwrap input without breaking existing apps?” I guess I can create a dom2 namespace 😕

tony.kay02:04:13

another option would be to assume people want :`synchronous?` on if transact is given a component. The end result would be that transactions would always immediately run optimistic actions and no longer be delayed into a submission queue…and that could change application runtime behavior, but perhaps only if you had set optimistic? to false…which I could make incompatible with sync true. Hm…sounding iffy.

bbss03:04:08

Great news. One input related thing I've run into a couple times, recently also with the forms that RAD generates. If I type really fast it skips some characters. This doesn't happen when using :defaultValue as opposed to :value. I'm guessing this might fix that as well?

tony.kay03:04:02

for sure @bbss

🎉 4
tony.kay03:04:27

it will also make it possible to use vanilla js input controls that have the same kinds of problems in Fulcro with no problems

tony.kay03:04:34

Very excited about this

tony.kay03:04:01

I’ve been avoiding this issue for some time because I hated the solutions…I finally hunkered down for 2 days and studied the problem…glad I did

tony.kay03:04:25

I’m now getting sub-ms update times in forms, even though I’m not (visibly) using component local state in the app

bbss03:04:30

that's great news 🙂

tony.kay03:04:39

I just have this “dom2” problem

bbss03:04:23

I don't mind using a new namespace, in general I think that approach of accretion to prevent breakage makes a lot of sense.

tony.kay03:04:37

I just hate clutter

bbss03:04:54

Understood, then maybe the question is, are people who'd experience breakage better off fixing this?

tony.kay03:04:26

well, I’m hoping to avoid breakage. I’ve actually already written dom2…I’m just hesitant to release it that way. I guess I’ll sleep on it

tony.kay03:04:00

I just cannot convince myself that defaulting transact to synchronous is “safe”…if I could do that, it’s a less cluttered soln

tony.kay03:04:24

oh, in fact, I know it isn’t…so there’s that 🙂

bbss03:04:06

Different topic, I want to upload an image and started diving into the code of rad-example. I noticed files work, but images are still a work in progress, why is that a different approach from files, shouldn't it be mostly the same except maybe for a preview of the image in the view?

tony.kay03:04:40

yeah..the problem is timing. If you set the URL on the IMG tag before the image is serve-able then you get a broken image

tony.kay03:04:05

I guess you could add a random query param to the image name and morph that to get it to retry

tony.kay03:04:42

I just hadn’t worked through it…you’re right that it should not be hard

bbss03:04:02

shouldn't be hard but some small hidden thing like that comes to bite 🙂

tony.kay03:04:32

yeah. The image names end up being SHAs…one approach I’ve done before is to render a thumbnail in the client before uploading, and just show that

tony.kay03:04:40

I have cljs code laying around to do that

tony.kay03:04:23

then it is exactly the same as the file upload we have..just show that instead of the icon

bbss03:04:54

so you keep the js/File / js/ImageData and display that? and then if you reload the page, lose local state, you can assume the file will be available as url?

tony.kay03:04:43

Here’s the ns that has that…

tony.kay03:04:43

well, the main fn at least:

tony.kay03:04:46

(defn make-thumbnail
  "CLJS only.
  Generate a thumbnail version of the given image, constrained to the given target-size.

  Puts the result on the thumbnail channel as :error or a base64str data url."
  [js-file target-size thumbnail-channel]
  #?(:cljs
     (let [reader (js/FileReader.)
           died   (fn [] (async/put! thumbnail-channel :error))]
       (set! (.-onerror reader) died)
       (set! (.-onload reader)
         (fn [evt]
           (let [url   (.. evt -target -result)
                 image (js/Image.)]
             (set! (.-onerror image) died)
             (set! (.-onload image)
               (fn [& args]
                 (let [image-width   (.-width image)
                       image-height  (.-height image)
                       aspect        (if (and image-width image-height)
                                       (/ image-height image-width)
                                       1.0)
                       target-width  (if (> image-width image-height)
                                       target-size
                                       (int (/ target-size aspect)))
                       target-height (if (> image-width image-height)
                                       (int (* target-size aspect))
                                       target-size)
                       canvas        (upsert-hidden-canvas "thumbnail-canvas" target-width target-height)
                       gctx          (.getContext canvas "2d")]
                   (.drawImage gctx image 0 0 target-width target-height)
                   (async/put! thumbnail-channel (.toDataURL canvas "image/jpeg" 0.6)))))
             (set! (.-src image) url))))
       (.readAsDataURL reader js-file))))

tony.kay03:04:41

and the upsert of a hidden canvas is:

(defn upsert-hidden-canvas
  "Upsert a canvas with the given ID, width, and height to the DOM, but position it off-screen.  Used for rendering
  off-screen to generate things like thumbnails.

  Returns the canvas.

  CLJS only."
  [id width height]
  #?(:cljs
     (when-let [existing-canvas (.getElementById js/document id)]
       (.remove existing-canvas)))
  #?(:cljs
     (let [canvas (.createElement js/document "canvas")
           style  (.-style canvas)]
       (.setAttribute canvas "id" id)
       (.setAttribute canvas "width" width)
       (.setAttribute canvas "height" height)
       (set! (.-position style) "fixed")
       (set! (.-top style) "0px")
       (set! (.-left style) (str "-" (* 2 width) "px"))
       (.appendChild js/document.body canvas))))

tony.kay03:04:02

oh, and I guess this helps put it all in context:

(defn compute-upload-details
  "CLJS only. Computes a SHA256 and thumbnail (if possible), and submits the file, thumbnail, sha256 to the file upload system.
   This is an asynchronous call, since file reading is. The callback will be called when the file read and
   calculations are complete, and will be passed:

   {:sha256 (computed-sha | :error)
    :js-file ^js File
    :thumbnail (string-encoded-data-url | :error)}

   You can adjust scoped behavior of this via *computational-timeout-ms* and *thumbnail-dimension*."
  [js-file callback]
  #?(:cljs
     (if (> (.-size js-file) *file-size-limit-bytes*)
       (callback {:sha256 :error :thumbnail :error :js-file js-file})
       (let [reader            (js/FileReader.)
             thumbnail-channel (async/chan 1)
             sha256-channel    (async/chan 1)]
         (make-thumbnail js-file *thumbnail-dimension* thumbnail-channel)
         (set! (.-onerror reader) #(async/put! sha256-channel :error))
         (set! (.-onload reader)
           (fn [evt]
             (let [blob (.. evt -target -result)]
               (async/put! sha256-channel (sha256 (js/Uint8Array. blob))))))
         (.readAsArrayBuffer reader js-file)
         (async/go
           (let [start     (inst-ms (js/Date.))
                 sha       (async/alt! [sha256-channel (async/timeout *computational-timeout-ms*)] ([v] (if v v :error)))
                 thumbnail (async/alt! [thumbnail-channel (async/timeout *computational-timeout-ms*)] ([v] (if v v :error)))
                 end       (inst-ms (js/Date.))]
             (timbre/debug "Computation of SHA and thumbnail took" (- end start) "ms")
             (callback {:sha256    sha
                        :js-file   js-file
                        :thumbnail thumbnail})))))))

tony.kay03:04:06

That was never in production, so it still has timings and things. Was worried that the sha computation would be slow in js, but it turned out to not really be the case…not sure if I tested with huge images.

bbss03:04:47

Is this something that should end-up in fulcro-rad?

tony.kay03:04:04

probably 🙂

bbss03:04:26

I have plans to build some very basic drawing over and selecting parts of images (ocr/translation stuff that google gives me). So a lot of this code is very useful to me. I'd like to contribute back somehow but find it hard to fit it into the bigger picture of fulcro-rad.

bbss03:04:56

Same story with the datomic-cloud/datahike stuff.

tony.kay03:04:13

So, consider this: RAD is intended to allow you to custom-design any kind of reusable UI control over top of any kind of data. To me your use-case might be a particular multi-field renderer that shows an image, lets you draw over it, and records the drawing for save.

tony.kay03:04:21

you can plug layouts

tony.kay03:04:42

so, technically a lot of different application views fit under the “form” or “report” model

tony.kay03:04:51

it’ll become more obvious in the coming weeks. I’m getting close to happy with the base forms and reports, and am about to expand into container land and also demo more advanced usage.

tony.kay03:04:49

It might be fun to do a simple drawing program in RAD, where the drawing is saved by save-form 🙂

bbss04:04:25

My app currently uses react-three-fiber and I have a fulcro-rad form embedded, pretty fancy! I'm curious to see if I can keep using the super powerful form-building stuff once the model becomes more complicated. I'll need to start doing the view/ui-code myself (can't keep relying on the generated ui). I'm hoping that I can keep using at least the form save/delete as I start nesting forms that have their own view.

tony.kay04:04:40

that’s the point, though you may also find that simply designing pluggable UI (that is reusable) is good as well.

tony.kay04:04:30

many ways to escape…the easiest is just to drop DOM right in place and use the functions from form ns to do the data manipulations.

bbss04:04:38

That's what I was planning on doing, dropping dom right in. Just worried that might not keep working as the form becomes more complicated. Anyway so far really happy with it. fulcro-rad is shaping up to be what I imagined a go-to toolset for great UI's to be like.

👍 4
tony.kay04:04:51

I pushed 3.2.0-SNAPSHOT of Fulcro to clojars with a dom2 ns. I’ll wait for feedback before committing to that. The mutation helpers like set-string! are also updated to work with dom2. You can use your own mutations in onChange and such by passing an options map with :synchronous? true. So, the mechanical update is just to add that option to all transacts inside of dom input event handlers, and use dom2 instead of dom. Much faster input interactions, and much better overall behavior. That should also resolve problems with js React libraries that have input-like controls that you’ve seen cursor jump problems on.

👍 4
Robin Jakobsson08:04:47

This makes me really excited! Looking forward to testing it.

tony.kay14:04:50

So, I’m going to drop dom2 and go with a compiler option instead. The new behaviors should work ok with wrapped inputs, just slower. Adding the compile option will keep the API surface area tighter. At some point in the future we can change the default for the compiler option…or not, doesn’t really matter.

cjmurphy04:04:36

And for com.fulcrologic.fulcro-css.localized-dom ?

tony.kay04:04:49

don’t use it for inputs? 😛

cjmurphy04:04:28

I've just been using it for everything.

tony.kay04:04:30

we have classname destructuring after all

tony.kay04:04:56

we can add raw-input or something to it? Not sure. Cat to be skinned another day

😿 4
tony.kay14:04:45

So, the approach I came up with this morning should make it easy to apply to ldom

tony.kay16:04:21

I just updated 3.2.0-SNAPSHOT on clojars. @cjmurphy @thosmos @ctamayo @lilactown After sleeping on it I realized I can make the new raw input support a completely transparent and non-breaking change:

- Added `transact!` open `:synchronous?` which allows for use of raw inputs without cursor jumping, and with much faster performance.
    - Added `transact!!` as a shorthand for turning on sync option in `transact!`
    - Added compiler option `:wrap-inputs?` to switch DOM to generate raw inputs. Defaults to true for bw compat.
    - Added compiler option `:default-to-hooks?` to enable hooks by default on `defsc`.
    - Changed mutation helpers to use `transact!!`
    - Formalized props tunneling API into `comp/tunnel-props!
` If you do nothing but upgrade then you can immediately use transact!! for all of the benefits. You’ll still have wrapped inputs (with a bit of extra overhead), but it works fine. If you go to your shadow-cljs file and add compiler config for your cljs build:
:compiler-options {:external-config {:fulcro     {:wrap-inputs? false}}}
(and restart shadow-cljs…macro caching is a pain), then you’ll get raw react inputs from dom/input instead of wrapped ones. At that point you must make sure to port all of your DOM event handlers on inputs that change .-value so that they use the new synchronous transactions. I’ve not tested this heavily with scenarios where you do I/O in mutations, so I’ll leave this a SNAPSHOT until I get the chance to do that.

🎉 32
wilkerlucio16:04:46

I like this approach, I think will work fine for most things, there is just one case that I think could be problematic, it is in case someone tries to use a library that depends on some defaults, and global switching could make it incompatible, but given we don’t have that many things in Fulcro I believe this approach still leads to the lesser friction

tony.kay16:04:22

well, and you have the global switching option for that case

tony.kay16:04:35

and it isn’t that much slower to have the wrapped inputs “in the middle” of the mess

wilkerlucio16:04:58

I mean, think if there is a Fulcro 3 library that relies on non hook components as the default, but the user makes a new app and enables hooks by default, at that moment the library wouldn’t work anymore in the user code, makes sense?

wilkerlucio16:04:41

that seems a problem for library authors, the user config must match the library dev config, thinking more on this framing this seems a tricky situation

wilkerlucio16:04:42

do you know if it is possible to change this configuration in a per-namespace base? if library authors could specify at the namespace level the config they need, this could fix this global config confict situation

lilactown16:04:35

the way I’ve solved this before is to enable the new behavior through a feature flag in the macro, and then inform users to create their own macro that wraps it with the feature flag on by default

lilactown16:04:51

this way it doesn’t require a global flag, and allows lib and app authors to make decisions in isolation

wilkerlucio16:04:24

I guess for the hooks thing, we can just write a recommendation for library authors to always be explicit about it, the dom wrapper seems tricker, is there a way to force it ignoring the config?

tony.kay16:04:47

the dom wrapper is harmless

tony.kay18:04:43

So is the vote that I drop the compiler option for hooks?

tony.kay18:04:32

I guess I could add an option to match on ns pattern, but I have to get to real work at the pt…not time for more of this right now

wilkerlucio18:04:11

maybe remove the hook global config until we figure this out?

wilkerlucio18:04:40

and awesome work on the new sync transact!, excited to give it a try!

tony.kay18:04:30

yeah, that was a very nice find

tony.kay16:04:44

Summary: no dom2 namespace!

👏 12
tony.kay16:04:55

Another really great result of this is that operations like Drag and Drop and other kinds of animations and such that would normally want component-local state can now just use transact!! without performance worries. In fact, if you want to drop the overhead of transactions altogether you can technically use tunnel-props! to get even faster component-local state speeds while still looking like you’re using props. That’s a very advanced usage, and requires you understand the basis-t that is used for the tunneling.

❤️ 16
currentoor16:04:41

did you mean transact!!?

tony.kay18:04:07

I’ve reverted the :default-to-hooks? option, and tested the transact!! in real applications with I/O. Seems fine.

rodolfo20:04:42

Hey folks, we’re using Fulcro 3 for an internal application at Nubank, it’s being a really pleasant experience so far. Kudos to all involved for this great piece of software, the only ones I know personally so far @tony.kay and @currentoor, and to @wilkerlucio for being our reference here in Nubank, both in Fulcro and Pathom stuff. I must say I’m sold on the fundamental ideas, and front-end state management was always something I didn’t quite like in anything I used before, even with react + redux. Now to my question: I’m curious whether someone has thought about or discussed providing some first-class support for URL query parameters (either in the URL itself or in inside its hash). The reason is: we’re starting to use them more and more, so far it’s manageable since we wrote the code to do it in our own namespace. But, for page transitions to keep or discard current query params, and fetching them from the current query params in :will-enter fns could become less manageable as the application grows. I can share some example code later if anyone is interested, but for now wanted to just know if anyone has thought about this or has done it in their applications. We’re hooking up our entrypoint fn in the "navigate" event in the History API. Thanks!

👍 4
❤️ 4
thosmos22:04:26

Food for thought: one thing I would do is parse query params in the URL parser before calling the fulcro router (and therefore outside of :will-enter), which enables one to check various things, re-route, etc.

thosmos22:04:33

Also, by handling the URL before Fulcro, I was able to create a component option called :check-session on a few route-targets to add an extra layer of checking that gets called before routing and before :will-enter https://github.com/thosmos/riverdb/blob/218bb6da75fbc595910ba4ea63093c2908ec4316/src/main/riverdb/ui/routes.cljs#L67

tony.kay23:04:42

Right…note the new stuff in Dynamic routing @thosmos…make it easier to check if a route is doable before doing it…I use that in RAD to “deny” routes (or restore them if the browser changed the URL)

thosmos23:04:54

oh cool I’ll check it out

tony.kay20:04:58

@rodolfo I just added query params with HTML5 routing to the fulcro-rad-demo

tony.kay20:04:28

it tracks things like current report parameters to make bookmark-compatible URLs

rodolfo20:04:19

Cool, I’ll check it out (as soon as Github comes back up)

tony.kay20:04:13

develop branch of demo, which is using all latest snapshots

👍 4
dvingo21:04:08

Basic input override of :value question. I have the following component and mutation:

(defmutation update-input-value
  [{:keys [val] :as props}]
  (action [{:keys [state]}]
    (swap! state
      (fn [s]
        (-> s (assoc-in [:component/id :test-input :input-value] "override"))))))

(defsc TestInput [this {:keys [input-value]}]
  {:query [:input-value]
   :ident (fn [] [:component/id :test-input])
   :route-segment ["test"]
   :initial-state (fn [_] {:input-value ""})}
   (dom/div
     (dom/p nil "Input is: " input-value)
     (dom/input {:value input-value
                 :onChange #(prim/transact! this [(update-input-value {:val (.. % -target -value )})])})))

(def ui-test-input (prim/factory TestInput))
On first keypress the value is rendered in the UI as "override". After that, typing in the input will ignore :value and just render whatever I'm typing. I am using this component within a router, I'm not sure if that matters.

dvingo22:04:20

I tried outside a router and I'm seeing the same behaviour. Also seeing the same when using mutation/set-string!. The value to the input-value prop is correct, being "override", but the input is still rendering the incorrect value

dvingo22:04:49

found a "solution", it's not great:

(react/createElement "input" #js{:value input-value
                 :onChange #(prim/transact! this [(update-input-value {:val (.. % -target -value )})

tony.kay22:04:54

@danvingo are you trying to report a bug on the snapshot, or a newbie question?

dvingo23:04:30

just a newb question - use of the constant is to demonstrate the issue. The value in the input is not the constant value "override" in this case. Whatever you type will show up in the input field instead of the value from props

dvingo23:04:46

using set-string! has the same issue

tony.kay23:04:20

ah, that actually has more to do with wrapping. It’s the stuff I’ve been talking about the past two days n the channel.

tony.kay23:04:32

you’re trying to “prove” that the input is controlled

tony.kay23:04:11

but in order for the input to work properly with async updates (which React hates) the Fulcro inputs are buffered by component local state…it’s a hack that every lib out there does in some shape in order to deal with the problem. I’ve never liked it. It’s technically a bug that the value doesn’t stay constant, and it would self-correct if you were actually chaning things via state. The 3.2.0 release has a new way of doing it that allows wrapped inputs to go away.

tony.kay23:04:05

so, you’ve technically discovered a bug, but in 3 yrs no one has complained about it before because that particular use-case just doesn’t matter. If you want a read-only field you mark it read only.

tony.kay23:04:46

The better answer, though, is that you’re asking a well-timed question, and 3.2 has a better solution. If you turn off wrapped inputs in 3.2 and use transact!!, then it will work as expected.

dvingo00:04:04

ha, well that's perfect then

dvingo01:04:34

Just to clarify the functionality I am trying to achieve is to adjust the input value in the transaction:

(defn to-int [str-num]
  (let [v (string/replace str-num #"[^0-9]" "")]
    (cond-> v
            (not= v "")
            (js/parseInt 10))))

(defmutation set-user-answer [{:keys [answer]}]
  (action [{:keys [state]}]
          (swap! state (fn [s]
                         (-> s (assoc-in [:component/id :my-component :value/user-answer] (to-int answer)))))))
I may not fully grasp what the core issue is, or if there are use-cases I'm not considering, but using a plain react input works just fine.

dvingo02:04:53

ah i see now, the cursor jumping is happening.. ok cool I'll try the new sync transact, thanks!

tony.kay02:04:29

So, nothing will help the cursor jumping in that use-case (changing the value as the user types) because it requires a real set of .-value on the real input DOM node…no vanilla js library on the planet can do it. period. The only way to do it is to write custom cursor positioning logic.

tony.kay02:04:57

Browsers do that to you…any time you cause a real set! of .-value on a real DOM input the cursor will jump

tony.kay02:04:31

React avoids calling set by not doing it if it sees you’re just updating it to what it already is. I wrote this that might help you understand: https://github.com/fulcrologic/fulcro/blob/develop/src/main/com/fulcrologic/fulcro/dom/DOMInputs.adoc

tony.kay22:04:49

your mutation is always setting it to a literal string constant?

tony.kay22:04:04

why is that what you want?

tony.kay22:04:15

(assoc-in [:component/id :test-input :input-value] "override")

tony.kay22:04:59

BTW, the mutations ns has (set-string! this :input-value :event %) as a shorthand for exactly this

Frank Henard22:04:52

Any tips on representing a to-one relationship. Eg. payee:

{:payee/id 1
 :payee/name "Amazon"}
transaction:
{:transaction/id 1
 :transaction/description "toilet paper"
 :transaction/payee {:payee/id 1}}
I've tried that, and I'm not figuring out how to do the query.

Frank Henard23:04:09

Here's what I think the query might look like:

[{:all-transactions [:transaction/description {:transaction/payee [:payee/name]}]}]

tony.kay23:04:09

yeah, that looks basically right

👍 4
Frank Henard01:04:20

Thanks! I was using Fulcro Inspect / Query, and the payee properties didn't pop up, so I assumed I was doing something wrong.