Fulcro Incubator
This is a set of features/utilities for Fulcro that are considered useful but have either not yet reached maturity
or are intended to be forked into new projects in the future.
We are following a strict usability plan with this library: we will refrain from making breaking changes/removal to
anything within, but will instead generate new namespaces for variations. This may bloat this particular library a bit,
but will ensure you can safely use it in production environments. Please let us know when you feel a feature
is mature, and we’ll consider accelerating promoting it to the main library or to its own smaller private project.
So, if you see something in here that you want to use: please feel free to do so. The worst thing that can happen is
it proves unpopular and you have to adopt some of it into your source base later, but that’s better
than having to write it from scratch, right?
I’ve been wanting to add state machine stuff to Fulcro for some time. I’ve finally done it, and I wish I’d done
it years ago. I’ve rewritten just a couple of things with it so far, and the results are so nice I can’t wait
to promote this thing out to Fulcro or its own library.
Try them for your login form, and I bet you’ll suddenly want to use them everywhere!
A new routing system based on dynamic queries with various cool features is in the dynamic-routing namespace. See
the documentation at dynamic-routing.adoc
The fulcro.incubator.db-helpers
has helper functions to deal with Fulcro local database map format. Those are helper functions for common operations like creating
a new entity, recursively removing data and it’s references.
The documentation for the functions is in the code, give a scan there to check what’s available.
|
The latter half of this file has things that are migrating to pessimistic-mutations. Use those instead.
|
|
Requires Fulcro 2.6.9 or greater.
|
Incubator includes an API for extended pessimistic mutations. These have features like automatic loading markers,
error state merging, and initialization help. The functions can be found in
and the workspaces have a demo (which is more revealing if you have Inspect installed in Chrome and watch the app state).
The basic idea is as follows:
-
A mutation action
can do an optimistic change.
-
A mutation ok-action
runs after the remote completes, and will see the value returned by the mutation under ::pm/mutation-response in
the entity against which the pmutate! was run. The mutation response will be cleared afterwards.
-
Errors call the error-action
part of the mutation. The error will be visible in app state during this handler, and
will remain under ::pm/mutation-response until the mutation is run again or it is manually cleared.
So, a sample pessimistic mutation looks like this:
(ns app
(:require
[fulcro.client.mutations :as m]
[fulcro.incubator.pessimistic-mutations :as pm]))
(m/defmutation do-something [_]
(action [env]
(js/console.log "Optimistic update (if needed)"))
(ok-action [env]
(js/console.log "Runs only if mutation is OK"))
(error-action [env]
(js/console.log "Runs if mutation fails."))
(remote [env] (pm/pessimistic-mutation env)))
and while these beefed-up mutations could be run normally (in which case the ok/error actions are no-ops), they are
meant to be run with:
(pm/ptransact! this `do-something {:params 2 ::pm/key :Bummer })
The status is automatically merged into the component that is transacted against (via it’s ident) at the
::pm/mutation-response
key or at another key that you specified, and progress is merged with that component along the
way so that you can write UI wrappers to handle the rendering of the different states.
It can have the following values:
- In progress
-
your component’s state will include:
::pm/mutation-response {::pm/status :loading }
The status field will always be present as long as the mutation response is present.
- OK
-
The ::pm/mutation-response
key will be there (and contain the value returned by the mutation)
during the ok-action
, but will be cleared once the mutation is complete.
- API Failure
-
(The mutation on the remote returned a map containing the key ::pm/mutation-errors):
The mutation response will look like this:
::pm/mutation-response {
... ; All of the things your mutation returned will be merged into this map
::pm/status :api-error ; YOUR API's contained the key ::pm/mutation-errors
::pm/key :Bummer ; This is the key set when you originally called pmutate!
::pm/mutation-errors :error-33} ; this came from your server mutation
where the error marker is a user-definable bit of data that you set up when you start the tx, and mutations-errors is
part of what the API call on the server returned. This will be visible to the error-action
, and will not be removed
from state unless you clear it or run another mutation against that component.
- Hard Failure
-
The server threw an exception or the network is down.
::pm/mutation-response {
::pm/status :hard-error ; network problem or exception thrown on server
::pm/key :Bummer ; your custom key, if set
::pm/low-level-error #error {:message "!!!"} ; error detail (if available)
:fulcro.client.primitives/ref [:demo/id 1]} ;ident of transacting component
and will be visible in error-action
and will not be removed until you clear it (or run another mutation against
that component).
If your application can have multiple mutations happening at the same time you will need to specify different mutation
response keys for each of them so they don’t clash. To do that you need to add the ::pm/mutation-response-key
to your pmutate!
parameters map:
(pm/pmutate! this `do-thing {::pm/mutation-response-key ::custom-mutation-response-key})
Do not use the normal Fulcro with-target
and returning
with pmutate!
, since you do not want those things to
happen on errors. The pmutate!
parameters (which are also sent to your handlers and the remote) can include a
special keys for doing targeting and merging:
-
::pm/returning Class
- If you include this in the params, then on an OK mutation response the response will be
merged with prim/merge-component
using the specified Class
.
-
::pm/target targets
- Exactly like Fulcro’s data fetch load targets (you can use df/multiple
, etc.)
(pm/pmutate! this `do-thing {::pm/returning TodoList
::pm/target (df/mutliple-targets
[:main-list]
(df/append-to [:all-lists]))})
The mutation-interface
namespace in this same library allows you to get rid of the
need to quote your mutation names. The pmutate!
call automatically detects these so that they
can be used:
(defmutation the-real-mutation [params] ...)
(mi/declare-mutation my-mutation `the-real-mutation)
...
(pm/pmutate! this `the-real-mutation {})
;; OR
(pm/pmutate! this my-mutation {})
UI components can share an ident (e.g. a PersonListItem
and a PersonForm
). If both are on the screen at the
same time and you use pmutate!
then both will see the mutation resposne in their state. Without a way
to distinguish the intended recipient of the response it would be hard to write components that behaved correctly
together on the screen.
To handle this scenario you can pass an additional ::pm/key
parameter to pmutate!
which will be included in the
::pm/mutation-response
at all phases that you can use in your UI to distinguish
which component should "pay attention to" the response. Of course all of the parameters are visible inside of the mutation itself,
but only the merged mutation response value is visible in the props
of the components for making rendering decisions
during the active phases. (they still have to include ::pm/mutation-response
in their query).
The special parameter ::pm/key
can be any (opaque and serializable) value.
Thus, two alternate renderings of the same state can deal with the idea of "localized mutations" (even though they will both
technically see the mutation response if they query for it):
(defsc Comp [this {::pm/keys [mutation-response]}]
{:query [::pm/mutation-response ...]
:ident (fn [] [:table :a]}
(let [{::pm/keys [key]} mutation-response]
(dom/div
(dom/button {:onClick #(pm/pmutate! this `do-thing {::pm/key :primary
:do-thing-param 2})})
(when (= :primary key) ...))))
(defsc CompAlt [this {::pm/keys [mutation-response]}]
{:query [::pm/mutation-response ...]
:ident (fn [] [:table :a]}
(let [{::pm/keys [key]} mutation-response]
(dom/div
(dom/button {:onClick #(pm/pmutate! this `do-thing {::pm/key :alt
:do-thing-param 1})})
(when (= :alt key) ...))))
Version 0.0.11 includes a ptransact!
in pessimistic-mutations
that works just like Fulcro’s ptransact!
,
but also supports the special behavior of pessimistic mutations (ok/error actions):
(pm/ptransact! this `[(local-mutation) (normal-remote-mutation) (pmutation) (other-mutation)])
will correctly delay at each remote-based mutation, and when it detects a mutation that is correctly
declared as a pessimistic mutation is will also trigger the proper error/ok actions.
When using the composition the default behavior is for the mutation to run all elements, even if one
has an error. In order to short-circuit, the error-action
(or follow-up mutation) must put something
in state that tells the remaining mutations not to run.
|
Your pmutations MUST return (pessimistic-mutation env) from a remote or they will not be
properly detected. Thus to short-circuit properly they should be written something like this:
|
(defmutation short-circuiting-mutation [_]
(ok-action [env] ...)
(remote [env]
(when-not (state-has-error-marker env)
(pm/pessimistic-mutation env))))
This ensures that detection should work (the detection sends empty state to the remote), but during operation
the actual state will cause it to keep from firing.
Fulcro supplies everything you need in order to show progress and errors, but
the addition of pmutate!
and a bit of standardization makes it possible for us to create helpers that make
flicker-free full-stack loading and mutation UI indicators.
When your server is fast, you don’t want to show a loading indicator. When it’s slow, you’d like the user to know
something is happening.
The support for arbitrary load markers in Fulcro’s load
, and targeted mutation response markers from pmutate!
make
this relatively easy. The steps are as follows:
-
Add a call to ui-progress/update-loading-visible!
in your componentDidUpdate
lifecycle method.
-
Add [fulcro.client.data-fetch/marker-table '_]
(for load progress) and :fulcro.incubator.pessimistic-mutations/mutation-response
(for mutation progress) to your component’s query.
-
Read the component local state value of :loading-visible?
in your component.
-
Render your progress marker if it is true.
-
When you issue loads, be sure to set the :marker
option of the load to your component’s ident.
-
Mutation progress is automatic with pmutate!
, as long as the mutation response is in the component query.
The flicker-free code will give you a delayed indicator, so if you use that to disable controls you’ll have
a time period where the user can press buttons.
The io-progress/busy?
function returns the immediate status of the component by looking at the
current props, and returns true if either a load OR mutation has started. It also requires the query to
contain the data fetch marker table and the pessimistic mutation response, as described above.
Since the setup above will put errors in a predictable location, we also provide these utility functions:
mutation-error
-
Returns false if there is no mutation error. When there is a mutation error it will attempt
to return the ::pm/mutation-errors
field. If that is not found, then it returns the entire mutation response.
load-error
-
Returns false if there is no load error, otherwise returns the Fulcro data fetch marker that is
in a failed state.
io-error
-
Returns false if there are no read/mutation errors (requires the query be correct). If there is an
error, it returns what load-error
or mutation-error
would have returned.
The io-progress
namespace also includes a Fulcro mutation for the client called clear-errors
, and
a mutation helper clear-errors*
that can be used on the state map. These can be used to clear out
the component-based mutation and read errors.
;; Clear any errors on this component
(prim/transact this `[(clear-errors)])
Currently this library requires usage of Shadow CLJS for compilation, this is due the
direct use of libraries from NPM that are not available in cljsjs.
To explore the things here, clone this project and run:
npm install
npx shadow-cljs watch workspaces
Copyright (c) 2018, Fulcrologic, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.