Skip to content

Latest commit

 

History

History
374 lines (265 loc) · 17.1 KB

README.adoc

File metadata and controls

374 lines (265 loc) · 17.1 KB

Fulcro Incubator

Important
Version 0.0.32 REQUIRES Fulcro 2.8.8 or better.

fulcro incubator fulcro incubator CircleCI (release) CircleCI (develop)

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.

Can I Use Them in Production?

We are following a maximal usability plan with this library: we will refrain from making breaking changes/removal to anything within, but will instead generate new namespaces for variations (we make make exceptions for tiny corrections that have minimal impact). 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?

State Machines

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.

For now it is here, and the documentation is in the root of the repository in adoc format, and also available in HTML.

Try them for your login form, and I bet you’ll suddenly want to use them everywhere!

Improved UI Routing

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, or in the HTML file.

DB Helpers

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.

Note
The latter half of this file has things that are migrating to pessimistic-mutations. Use those instead.

Pessimistic Mutations

Note
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).

Basics

The basic idea is as follows:

  1. A mutation action can do an optimistic change.

  2. 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.

  3. 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 })

Responses and Status

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})

Targeting and Merging the Mutation Response

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.)

For example:

(pm/pmutate! this `do-thing {::pm/returning TodoList
                             ::pm/target (df/multiple-targets
                                           [:main-list]
                                           (df/append-to [:all-lists]))})

Leveraging Mutation Interfaces

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 {})

Dealing with Ident Overlap

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) ...))))

Composition

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.

Aborting a Sequence

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.

Warning
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.

Flicker-Free IO Progress and Errors

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:

  1. Add a call to ui-progress/update-loading-visible! in your componentDidUpdate lifecycle method.

    • Optionally set the load marker name and timeout via the optional parameter map.

    • (NOT IMPLEMENTED YET) Optionally set the :key to distinguish instances (TODO)

  2. 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.

  3. Read the component local state value of :loading-visible? in your component.

  4. Render your progress marker if it is true.

  5. When you issue loads, be sure to set the :marker option of the load to your component’s ident.

  6. Mutation progress is automatic with pmutate!, as long as the mutation response is in the component query.

Preventing Double-submission

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.

Reading Errors

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.

Clearing Errors

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)])

Example

There is a full working example flicker_free_ws.cljs in the workspace cards.

UI

[fulcro.incubator.ui.core](https://github.com/fulcrologic/fulcro-incubator/blob/develop/src/main/fulcro/incubator/ui/core.cljs) contains functions to help using React components with Fulcro.

Reakit

You can use [Reakit](https://reakit.io/) wrapped with Fulcro DOM CSS support from [fulcro.incubator.ui.reakit](https://github.com/fulcrologic/fulcro-incubator/blob/develop/src/main/fulcro/incubator/ui/reakit.cljs).

React Icons

[React icons](http://react-icons.github.io/react-icons/) support is provided via [fulcro.incubator.ui.icons.*](https://github.com/fulcrologic/fulcro-incubator/tree/develop/src/main/fulcro/incubator/ui/icons) namespaces, just refer to the functions there to use the icons directly.

Shadow CLJS required

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.

Compiling workspaces

To explore the things here, clone this project and run:

npm install
npx shadow-cljs watch workspaces

Then navigate to

http://localhost:3689/

You can view a precompiled version of the workspaces on [github.io](https://fulcrologic.github.io/fulcro-incubator/)

Copyright

Copyright (c) 2018, Fulcrologic, LLC

The MIT License (MIT)

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.