Workspaces is a component development environment for ClojureScript, inspired by devcards.
First add the workspaces dependency on your project.
This is recommended template for the HTML file for the workspaces:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<!-- you might need to change the js path depending on your configuration -->
<script src="/js/workspaces/main.js" type="text/javascript"></script>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/github.min.css">
</body>
</html>
Then create the entry point for the workspaces:
(ns my-app.workspaces.main
(:require [nubank.workspaces.core :as ws]
; require your cards namespaces here
))
(defonce init (ws/mount))
;; shadow-cljs configuration
{:builds {:workspaces {:target :browser
:output-dir "resources/public/js/workspaces"
:asset-path "/js/workspaces"
:devtools {:before-load nubank.workspaces.core/before-load
:after-load nubank.workspaces.core/after-load
:http-root "resources/public"
:http-port 3689
:http-resource-root "."}
:modules {:main {:entries [my-app.workspaces.main]}}}}}
Now run with:
npx shadow-cljs watch workspaces
For some reason on my tests I couldn't make Figwheel :on-jsload
hook to work with library
code, to go around this modify the main to look like this:
(ns my-app.workspaces.main
(:require [nubank.workspaces.core :as ws]
; require your cards namespaces here
))
(defn on-js-load [] (ws/after-load))
(defonce init (ws/mount))
So you can call the hook forom there, as:
:cljsbuild {:builds
[{:id "dev"
:source-paths ["src"]
:figwheel {:on-jsload "myapp.workspaces.main/on-js-reload"}
:compiler {:main myapp.workspaces.main
:asset-path "js/workspaces/out"
:output-to "resources/public/js/workspaces/main.js"
:output-dir "resources/public/js/workspaces/out"
:source-map-timestamp true
:preloads [devtools.preload]}}]}
Now run with:
lein figwheel
To define cards you use the ws/defcard
macro, here is an example to create a React card:
(ns myapp.workspaces.cards
(:require [nubank.workspaces.core :as ws]
[nubank.workspaces.card-types.react :as ct.react]))
; simple function to create react elemnents
(defn element [name props & children]
(apply js/React.createElement name (clj->js props) children))
(ws/defcard hello-card
(ct.react/react-card
(element "div" {} "Hello World")))
You can use this to mount any React component, for a re-frame for example, you can
use (reagent/as-element [re-frame-root])
as the content.
Usually libraries like Fulcro or Re-frame will manage the state and trigger render in the proper times, but if you wanna do something with raw React, you can provide an atom to be the app state, and the card will watch that atom and triggers a root render everytime it changes.
(ws/defcard counter-example-card
(let [counter (atom 0)]
(ct.react/react-card
counter
(element "div" {}
(str "Count: " @counter)
(element "button" {:onClick #(swap! counter inc)} "+")))))
Workspaces is built with Fulcro and has some extra support for it. Using the fulcro-card
you can easely mount a Fulcro component with the entire app, here is an example:
(ns myapp.workspaces.fulcro-demo-cards
(:require [fulcro.client.primitives :as fp]
[fulcro.client.localized-dom :as dom]
[nubank.workspaces.core :as ws]
[nubank.workspaces.card-types.fulcro :as ct.fulcro]
[nubank.workspaces.lib.fulcro-portal :as f.portal]
[fulcro.client.mutations :as fm]))
(fp/defsc FulcroDemo
[this {:keys [counter]}]
{:initial-state (fn [_] {:counter 0})
:ident (fn [] [::id "singleton"])
:query [:counter]}
(dom/div
(str "Fulcro counter demo [" counter "]")
(dom/button {:onClick #(fm/set-value! this :counter (inc counter))} "+")))
(ws/defcard fulcro-demo-card
(ct.fulcro/fulcro-card
{::f.portal/root FulcroDemo}))
By default the Fulcro card will wrap your component will a thin root, by having always
having components with idents you can leverage generic mutations, this is recommended
over making a special Root. But if you want to send your own root, you can set the
::f.porta/wrap-root? false
. Here are more options available:
::f.portal/wrap-root?
(default: true
) Wraps component into a light root::f.portal/app
(default: {}
) This is the app configuration, same options you could send to fulcro/new-fulcro-client
::f.portal/initial-state
(default {}
) Accepts a value or a function. A value will
be used to call the initial state function of your root. If you provide a function, the
value returned by it will be the initial state.When you use a Fulcro card you will notice it has an extra toolbar, in this toolbar you have two action buttons:
Inspect
: this is an integration with Fulcro Inspect, if you have the
extension active on Chrome, it will select the application of the card for inspection.Restart
: this will do a full refresh on app, unmount and mount againWorkspaces has default integration with cljs.test
, but you have to start the tests
using ws/deftest
instead of cljs.test/deftest
. The ws/deftest
will also emit a
cljs.test/deftest
call, so you can use the same for running on CI. Example test card:
(ws/deftest sample-test
(is (= 1 1)))
When you create test cards using ws/deftest
, a card will be automatically created to
run on the test on that namespace, just click on the test namespace name on the index
to load the card.
You can define settings for your card, like what initial size it should have, to do that you can add maps to the card definition:
(ns myapp.workspaces.configurated-cards
(:require [nubank.workspaces.core :as ws]
[nubank.workspaces.model :as wsm]))
(ws/defcard sized-card
{::wsm/card-width 5
::wsm/card-height 7}
(ct.react/react-card
(dom/div "Foo")))
The measuremnt is in grid tiles. A recommended way to define a card size is to add it
in default size to workspace, resize it to the appropriated size, then use the Size
button accessible from the more icon in the card header, the card current size will
be logged to the browser console.
For the built-in cards you can also determine how the element will be positioned in the card. So far we have been using the center card position but depending on the kind of component you are trying that might not be the best option.
(ws/defcard positioned-top
{::wsm/card-width 5
::wsm/card-height 7
::wsm/align {:flex 1}}
(ct.react/react-card
(dom/div "Foo on top")))
The card container is a flex element, so the previous example will put the card on top and make it occupy the full width of the container.
The default ::wsm/align
is:
{:display "flex"
:align-items "center"
:justify-content "center"}
Using the key ::wsm/node-props
you can set the style or other properties of the container node.
(ws/defcard styles-card
{::wsm/node-props {:style {:background "red" :color "white"}}}
(ct.react/react-card
(dom/div "I'm in red")))
You will probably find some combinations of card settings you keep repeating, it's totally ok to
put those in variables and re-use. You can also send as many configuration maps as you
want, in fact the return of (ct.react/react-card)
is also a map, they all just get
merged and stored as the card definition.
(def purple-card {::wsm/node-props {:style {:background "#79649a"}}})
(def align-top {::wsm/align {:flex 1}})
(ws/defcard widget-card
{::wsm/card-width 3 ::wsm/card-height 7}
purple-card
align-top
(ct.react/react-card
(dom/div "💜")))
Now that we know how to define cards, it's time to learn how to work with then.
Imagine when you are about to start working on some components of your project, you can
start by looking at the index or searching using the spotlight feature (alt+shift+a
).
By clicking on the card names you will add then to the current workspace (one will be created if you don't have any open).
The idea here is that you add just the cards there are relevant to the work you need to do, and create a workspace that can make the best use of your screen pixels.
And workspaces comes on tabs, enabling you to quickly switch between different workspace settings.
The following topics will describe what you can do to help you manage your workspaces.
You can create new workspaces by clicking at the +
tab on the interface. The workspaces
are created and stored in your browser local storage. You can rename the workspace
by clicking on its tab while it's active.
Your cards are placed in a responsive grid, this means that the number of columns you
have available will vary according to your page width size. In the right below the
workspace tabs you can see how many columns you have available right now (eg: c8
means 8 columns).
Each responsive breakpoint will have stored separated, so you can arange a workspace to fit that available width. The sizes and positions will be recorded separated by each column numbers (they vary from 2 to 20).
Each column size has 120~140px, varies depending on page width.
When you have an open workspace, there is a toolbar with some action buttons, here is a description of what each does:
Copy layout
: actually a select here, use this to copy the layout from a different responsive breakpointRefresh cards
: triggers a refresh on every card on this active workspaceDuplicate
: creates a copy of current workspaceUnify layouts
: makes every breakpoint have the same layout as the current active oneExport
: Export current workspace layouts to data (logged into browser console)Delete
: Delete current workspaceA lot of times your workspaces will be disposable, just pull a few components, work and throw away. But other times you like to create more durable ones, like a kitchen sink of all your components buildings blocks, or maybe a setup that works nice for a specific task. You a lot of effort to make it look good on many different responsive breakpoints. So would be a pain if every user of the system had to redo the task to organize those types of workspaces.
To solve that, you can use the Export
button on the workspace toolbar. It outputs
the workspace layout as a transit data on the console. You can copy that, and use
to store that workspace setup on the code, making it available to any other person
using this workspace setup.
(ws/defworkspace ui-block
"[\"^ \",\"c10\",[[\"^ \",\"i\",\"~$fulcro.incubator.workspaces.ui.reakit-ws/reakit-base\",\"w\",2,\"h\",4,\"x\",0,\"y\",0,\"minH\",2]],\"c8\",[[\"^ \",\"i\",\"^0\",\"w\",2,\"h\",4,\"x\",0,\"y\",0,\"^1\",2]],\"c16\",[[\"^ \",\"i\",\"^0\",\"w\",2,\"h\",4,\"x\",0,\"y\",0,\"^1\",2]],\"c14\",[[\"^ \",\"i\",\"^0\",\"w\",2,\"h\",4,\"x\",0,\"y\",0,\"^1\",2]],\"c2\",[[\"^ \",\"i\",\"^0\",\"w\",2,\"h\",4,\"x\",0,\"y\",0,\"^1\",2]],\"c12\",[[\"^ \",\"i\",\"^0\",\"w\",2,\"h\",4,\"x\",0,\"y\",0,\"^1\",2]],\"c4\",[[\"^ \",\"i\",\"^0\",\"w\",2,\"h\",4,\"x\",0,\"y\",0,\"^1\",2]],\"c18\",[[\"^ \",\"i\",\"^0\",\"w\",2,\"h\",4,\"x\",0,\"y\",0,\"^1\",2]],\"c20\",[[\"^ \",\"i\",\"^0\",\"w\",2,\"h\",4,\"x\",0,\"y\",0,\"^1\",2]],\"c6\",[[\"^ \",\"i\",\"^0\",\"w\",2,\"h\",4,\"x\",0,\"y\",0,\"^1\",2]]]"))
When you open a shared workspace, you can't change it, it's static, but you can duplicate it and change the copy as you please.
Here is a list of available shortcuts, all of then use alt+shift
followed by a key:
alt+shift+a
: Add card to current workspace (open spotlight for card picking)alt+shift+i
: Toggle index viewalt+shift+h
: Toggle card headersalt+shift+n
: Create new local workspacealt+shift+w
: Close current workspaceCan you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close