nubank/workspaces

repository·master·Indexed 19 days ago

https://github.com/nubank/workspaces

A component development environment for ClojureScript that enables developers to create, organize, and interact with UI components (cards) and tests in a responsive, tabbed workspace. It supports React and Fulcro cards, provides a custom Shadow-CLJS build target for automated card discovery, and integrates with Figwheel. The library includes a card lifecycle system (init, render, refresh, dispose) and a custom test macro, ws/deftest, to integrate tests directly into the UI.

Tokens
2.7K
Snippets
11
Records
11
Agent score
18%

What's inside workspaces

  1. How to develop custom card types

    master

    A custom card type is a function that returns a map containing lifecycle hooks. You can create wrappers around existing card types (like React) to add custom behavior, such as timers or toolbars.

    To support alignment in custom cards, wrap your definition with ct.util/positioned-card.

    (defn react-timed-card [state-atom component]
      {::wsm/init #(react-timed-card-init % state-atom component)})
  2. How the Card lifecycle works

    master

    Cards follow a specific lifecycle managed by Workspaces:

    1. Initialization (::wsm/init): Called when a card is first placed in a visible workspace. It returns a map containing lifecycle functions. This is where you set up local state.
    2. Rendering (::wsm/render): Workspaces provides an HTML node; you use this function to mount your component.
    3. Refresh (::wsm/refresh): Triggered by the 'Refresh cards' action or code reloads. It allows you to force a re-render without full re-initialization.
    4. Dispose (::wsm/dispose): Called when a card is removed from all active workspaces. Use this to clean up resources like timers or event listeners.
    (ws/defcard custom-card
      {::wsm/init
       (fn [card]
         (let [counter (atom 0)]
           {::wsm/render
            (fn [node] (gdom/setTextContent node (str "Count: " @counter)))
            ::wsm/refresh
            (fn [node] (gdom/setTextContent node (str "Updated: " (swap! counter inc))))
            ::wsm/dispose
            (fn [node] (gdom/setTextContent node ""))}))})
  3. Install and Setup Workspaces

    master

    Workspaces is a component development environment for ClojureScript. To set it up:

    1. Add Dependency: Add nubank/workspaces to your project.
    2. HTML Template: Use a standard HTML structure with a container div (<div id="app"></div>) and include the Workspaces main JS file and highlight.js styles.
    3. Entry Point: Create a workspace entry point (e.g., my-app.workspaces.main) that calls (ws/mount).
    (ns my-app.workspaces.main
      (:require [nubank.workspaces.core :as ws]
                ; require your cards namespaces here
                ))
    
    (defonce init (ws/mount))
    <!-- Recommended HTML Template -->
    <!DOCTYPE html>
    <html lang="en">
      <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>
        <!-- adjust js path as needed -->
        <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>
  4. Configure Workspaces with Shadow-CLJS

    master

    Workspaces provides a custom Shadow-CLJS build target that automates card discovery and configuration. It scans for files matching a regex, configures the main module, and detects new files automatically.

    Custom Target Configuration: Use :target nubank.workspaces.shadow-cljs.target in your shadow-cljs.edn.

    ;; shadow-cljs configuration
    {:builds {:cards {:target     nubank.workspaces.shadow-cljs.target
                      :ns-regexp  "-(test|cards)$"
                      :output-dir "resources/public/js/workspaces"
                      :asset-path "/js/workspaces"
                      :preloads   [] ; optional
                      }}}
  5. Configure Workspaces with Figwheel

    master

    To use Workspaces with Figwheel, you must use the :on-jsload hook to call (ws/after-load) in your workspace entry point to ensure cards refresh correctly when code changes.

    (ns my-app.workspaces.main
      (:require [nubank.workspaces.core :as ws]))
    
    (defn on-js-load [] (ws/after-load))
    (defonce init (ws/mount))
    
    ;; figwheel configuration
    {:cljsbuild {:builds
                  [{:id           "dev"
                    :source-paths ["src"]
                    :figwheel     {:on-jsload "myapp.workspaces.main/on-js-load"}
                    :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]}}]}}
  6. Configure Card size, alignment, and styles

    master

    You can customize the appearance of a card by passing a configuration map as the first argument to ws/defcard.

    Available Configuration Keys:

    • ::wsm/card-width: Width in grid tiles.
    • ::wsm/card-height: Height in grid tiles.
    • ::wsm/align: Flexbox alignment (e.g., {:flex 1}).
    • ::wsm/node-props: Style or properties for the container node (e.g., {:style {:background "red"}}).
    (ws/defcard sized-card
      {::wsm/card-width 5
       ::wsm/card-height 7
       ::wsm/align {:flex 1}
       ::wsm/node-props {:style {:background "red"}}}
      (ct.react/react-card (dom/div "Styled Card")))
  7. Create Test cards with ws/deftest

    master

    To integrate tests into the Workspaces UI, use ws/deftest instead of cljs.test/deftest. This automatically creates a card for the test namespace and a test-all card for the entire suite.

    (ws/deftest sample-test
      (is (= 1 1)))
  8. Create Fulcro cards with ws/defcard

    master

    Workspaces has native support for Fulcro via ct.fulcro/fulcro-card. This allows you to mount a Fulcro component with its own app configuration and state.

    Fulcro Card Options:

    • ::f.portal/wrap-root? (default: true): Wraps component in a light root.
    • ::f.portal/app (default: {}): App configuration (same as fulcro/new-fulcro-client).
    • ::f.portal/initial-state (default: {}): Value or function for initial state.
    • ::f.portal/root-state (default: {}): Map merged into app root state.
    • ::f.portal/computed: Data added to root factory props.
    • ::f.portal/root-node-props: Props for the root node.
    (ws/defcard fulcro-demo-card
      (ct.fulcro/fulcro-card
        {::f.portal/root FulcroDemo}))
  9. Add a toolbar to a card

    master

    You can add a custom toolbar to a card by providing a ::wsm/render-toolbar function in your card's initialization map. This function should return a React component.

    (ws/defcard custom-card
      {::wsm/init
       (fn [card]
         (assoc
           {::wsm/render (fn [node] (gdom/setTextContent node "Hello"))}
           ::wsm/render-toolbar
           (fn [] (dom/div (dom/button {:onClick #(js/console.log "Clicked")} "Click Me")))))})
  10. Create React cards with ws/defcard

    master

    Use the ws/defcard macro combined with a card type (like ct.react/react-card) to define UI components.

    Stateful React Cards: You can pass an atom to react-card. The card will watch the atom and trigger a root render whenever the atom changes.

    (ns myapp.workspaces.cards
      (:require [nubank.workspaces.core :as ws]
                [nubank.workspaces.card-types.react :as ct.react]))
    
    ;; Simple React card
    (ws/defcard hello-card
      (ct.react/react-card
        (element "div" {} "Hello World")))
    
    ;; Stateful React card
    (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)} "+")))))