• Stars
    star
    194
  • Rank 200,219 (Top 4 %)
  • Language
    Clojure
  • License
    Eclipse Public Li...
  • Created over 2 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A ClojureScript system to build browser based frontends

shadow-grove

Clojars Project

shadow-grove is a combination of pieces required to build performant DOM-driven user interfaces, scaling from small to very large. The goal is to have something written in ClojureScript with no extra dependencies and excellent performance out of the box.

The core pieces are

  • shadow.arborist: Core Abstraction to create and update DOM segments
  • shadow.grove.db: Normalized simplistic DB, using just CLJS maps
  • shadow.grove.events: Event system to handle changes to the system
  • shadow.grove.components: The component system providing the basis to connect the pieces: dispatching events, reading from the DB (via EQL queries) and updating the DOM.

Try it live in your browser via the shadow-grove Playground.

Prior Art

Pretty much everything here borrows ideas from other libraries in the JS/CLJS space.

  • The DOM related parts borrow ideas from JS systems such as react, svelte, vue.
  • The event system is pretty similar to re-frame but uses event maps instead of vectors. Subscriptions are also replaced by EQL queries.
  • The normalized db and EQL query concepts were inspired by fulcro

However, all things here are written from scratch. Mostly since we are not using react which the other CLJS libs require.

Current Status

shadow.grove is far from finished but usable. Performance is good. I have been using this for a few years now. Documentation is still in an terrible state though.

The shadow-cljs UI is sort of a reference application for all of this. You can experience it live at http://localhost:9630 if you have shadow-cljs running locally. You can also use the grove-todo example.

Other react based CLJS libraries have far more features that shadow.grove is still missing. If you are using a lot of 3rd party react components you probably shouldn't be looking at this.

Quickstart

Using the shadow.grove implementation of TodoMVC (source here) as an example for getting started. You can just clone the template repo or copy what you need. Explanations of the different parts will follow.

The core structures in shadow.grove are modular so each piece needs to be setup separately. You only initialize what you need when you need it. The minimum we need to create is the database and the runtime holding out our other "state". I recommend creating this in a dedicated namespace.

(ns todo.ui.env
  (:require
    [shadow.grove :as sg]
    [shadow.grove.db :as db]
    [todo.model :as-alias m]))

(def schema
  {::m/todo
   {:type :entity
    :primary-key ::m/todo-id
    :attrs {}
    :joins {}}
   })

(defonce data-ref
  (-> {::m/id-seq 0
       ::m/editing nil}
      (db/configure schema)
      (atom)))

(defonce rt-ref
  (-> {}
      (sg/prepare data-ref :todo)))

This namespace can then potentially be used by everything else that may require access to env/rt-ref. It basically just holds all our application state.

In our main namespace we then initialize everything and render our views.

(ns todo.ui
  (:require
    [shadow.grove :as sg]
    [shadow.grove.history :as history]
    [todo.ui.env :as env]
    [todo.ui.views :as views]
    [todo.ui.db]))

(defonce root-el
  (js/document.getElementById "app"))

(defn render []
  (sg/render env/rt-ref root-el
    (views/ui-root)))

(defn init []
  ;; useful for debugging until there are actual tools for this
  (when ^boolean js/goog.DEBUG
    (swap! env/rt-ref assoc :shadow.grove.runtime/tx-reporter
      (fn [{:keys [event] :as report}]
        ;; alternatively use tap> and the shadow-cljs UI
        (js/console.log (:e event) event report))))

  (history/init! env/rt-ref
    {:use-fragment true
     :start-token "/all"})

  (render))

(defn ^:dev/after-load reload! []
  (render))

The views namespace contains all of our component code. Going over all of them here would get too long. Feel free to explore them though.

The significant two pieces are the defc and << macros.

  • << creates arborist fragments, which represent one or more elements for our tree.
  • defc creates components which manage data and events

Fragments

Fragments use the hiccup notation but are compiled down to JS code. You'll be using these a lot to represent DOM elements.

(<< [:h1 "Hello World"])

(defn snippet [foo bar]
  (<< [:div foo]
      [:div bar]))

(<< [:div "before"]
    (some-component 1 2 3)
    [:div "after"])

More on fragments here (TBD).

Components

Components are responsible for managing and rendering data. They also are the default target for events.

(defc ui-example [arg]
  (bind data
    (sg/query-root
      [:foo
       :bar]))

  (event ::foo! [env ev e]
    (sg/dispatch-up! env (assoc ev :data-taken-from-e ...)))
  
  (render
    (<< [:div arg]
        [:div {:on-click ::foo!} "click foo"]
        [:div (pr-str data)])))
  • bind represents a hook that injects data into the component. The signature is (bind <binding-form> &body-creating-hook).
  • event creates an event handler. Events in fragments are declared as just maps of data. :on-click ::foo! is short but equivalent to :on-click {:e ::foo!}. You may add additional key/value pairs as needed. All events are by default dispatched to the nearest component first and then bubble up the component tree.
  • render ultimately takes the data hooks injected and render it. The return value is expected to implement the shadow.arborist protocols (eg. Fragments).

More on components here (TBD).

Events

The event subsystem takes the event maps and effects changes to our runtime/db.

The event handler inside Components are really just meant to give you a point to extract DOM related data from its 3rd (ie. e above) argument, which is the native DOM event. They are not actually allowed to modify our DB otherwise. Once the data is extracted they can just pass the event along to be processed by the actual handler.

This isn't always necessary, so most of the time events will just bubble up to the runtime and the (event ...) handler in the component can be omitted.

You can register an event handler via the shadow.grove/reg-event function.

(sg/reg-event env/rt-ref ::foo!
  (fn [tx-env ev]
    (js/console.log ::foo! env ev)
    env))

tx-env represents the transaction environment. It does contain a :db key which represents our database. It can be modified using the usual clojure functions (eg. assoc, assoc-in update, update-in, etc).

The event handlers are expected to return the modified tx-env.

More on events here (TBD).

Database

The last essential piece is our normalized database, handled by a flat persistent map. Basically a key/value store that you assoc, dissoc and update values in. It is normalized to we avoid duplication of UI data and to keep access efficient.

The TodoMVC example from above doesn't really need a schema so let's assume we have a shop of some kind listing products. Each product has a manufacturer.

{:products
 [{:product-id 1
   :product-name "Foo"
   :manufacturer {:manufacturer-id 1
                  :manufacturer-name "ACME"}}
  {:product-id 2
   :product-name "Bar"
   :manufacturer {:manufacturer-id 1
                  :manufacturer-name "ACME"}}]}

If we were to store this shape in our database we would end up with duplicated data since both products are from the same manufacturer. This becomes messy and hard to work with over time. We also want more efficient access to data without having to traverse deeply into some nested datastructure.

Normalizing this db, we instead end up with

{:products
 [#gdb/ident [:product 1]
  #gdb/ident [:product 2]]
 
 #gdb/ident [:product 1]
 {:product-id 1
  :product-name "Foo"
  :manufacturer #gdb/ident [:manufacturer 1]}

 #gdb/ident [:product 2]
 {:product-id 2
  :product-name "Bar"
  :manufacturer #gdb/ident [:manufacturer 1]}
 
 #gdb/ident [:manufacturer 1]
 {:manufacturer-id 1
  :manufacturer-name "ACME"}}

Each entity is represented by an ident (using the #gdb/ident tag here). It is used as a reference and can be used to get the actual value from the DB.

Similar to other EQL like systems (eg. fulcro, om.next, pathom), idents are a combination of entity-type and id. You may also think of this as table-name and id when using a SQL database in the backend. Just like in SQL if you just have id=1 you don't know which table you need to get it from. Note that having multiple entity types is entirely optional, but convenient depending on the shape of your actual data requirements.

More on the database here (TBD).

Other useful pieces

  • Routing
  • Virtual Lists
  • Suspense

TBD.

More Repositories

1

shadow-cljs

ClojureScript compilation made easy
Clojure
2,147
star
2

shadow-arborist

Exploring a CLJS world without React, see shadow-grove repo instead.
Clojure
198
star
3

shadow-experiments

Archived. See shadow-grove.
Clojure
116
star
4

shadow-build

[DEPRECATED] merged into the thheller/shadow-cljs project
Clojure
100
star
5

shadow-css

CSS-in-CLJ(S)
Clojure
80
star
6

reagent-expo

test using reagent with expo/react-native
Clojure
51
star
7

next-cljs

Proof of concept: next + shadow-cljs [unmaintained]
Clojure
50
star
8

shadow

collection of useful CLJS code
Clojure
50
star
9

reagent-react-native

Example App using reagent with react-native via shadow-cljs
Java
47
star
10

gatsby-cljs

Proof of concept: gatsby + shadow-cljs [unmaintained]
CSS
32
star
11

shadow-cljsjs

Clojure
23
star
12

code-splitting-clojurescript

Example App using ClojureScript :modules and shadow-cljs
Clojure
21
star
13

wasm-pack-cljs

quick demo using wasm-pack generated wasm from CLJS
JavaScript
18
star
14

react-router-cljs

example app using react-router with reagent and shadow-cljs
Clojure
15
star
15

shadow-pgsql

PostgreSQL Client for the JVM
Java
15
star
16

fulcro-expo

test using fulcro with expo/react-native
Clojure
15
star
17

js-framework-shadow-grove

Clojure
14
star
18

reagent-pdfjs

Clojure
13
star
19

chrome-ext-v3

Clojure
11
star
20

netlify-cljs

demo frontend+function deployed to netlify
Clojure
10
star
21

shadow-graft

Clojure
10
star
22

reagent-react-integration

JavaScript
9
star
23

electron-cljs

Electron App Example in ClojureScript using shadow-cljs
Clojure
8
star
24

shadow-cljs-ext

Loading the shadow-cljs UI in browser devtools
JavaScript
8
star
25

lambda-cljs

AWS Lambda via shadow-cljs
Clojure
6
star
26

zpipe

Experiment: MsgPack + ZeroMQ == super simple RPC
Ruby
6
star
27

clojure-cli

clojure cli installable via npm
Clojure
5
star
28

shadow-undertow

undertow via clojure
Clojure
5
star
29

cordova-cljs

bare bones example using shadow-cljs with cordova
Clojure
4
star
30

lumifoo

luminus template output ported to shadow-cljs
Clojure
3
star
31

grove-todo

Clojure
3
star
32

cassandra-erl

UNMAINTAINED! hopefully some kind of usable cassandra client, hiding the horrible thrift api.
Erlang
3
star
33

thrift-erl

UNMAINTAINED! extracted from apache/thrift, just the erl bindings
Erlang
3
star
34

cljs-cf-worker

Clojure
2
star
35

cljs-protobuf

Example using protobuf in a CLJS project
JavaScript
2
star
36

regl-example

Clojure
2
star
37

timed-counter

simple Counters in redis
Ruby
2
star
38

gen_http

Erlang Web Framework Experiments
Erlang
1
star
39

shadow-mui-dashboard-test

JavaScript
1
star
40

cljs-issues

JavaScript
1
star
41

zprint-npm

Clojure
1
star
42

shadow-re-frame

Starting point for ClojureScript apps with shadow-cljs, proto-repl, and re-frame.
Clojure
1
star
43

p5-tiledmap-demo

JavaScript
1
star
44

cljs-i18n-api

API proposal for compiler-aided i18n for CLJS (draft)
Clojure
1
star
45

code-splitting-challenge

Clojure
1
star
46

fulcro-questions

some fulcro experiments/questions
HTML
1
star
47

tagged-exchange

RabbitMQ tagged exchange
Erlang
1
star
48

zview

zview - Erlang Template Engine (inspired by erlydtl, Django Templates, Liquid and others.)
Erlang
1
star