Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/sansarip/peanuts

Packing peanuts for Reagent/Re-frame components
https://github.com/sansarip/peanuts

clojure clojurescript re-frame reagent

Last synced: 22 days ago
JSON representation

Packing peanuts for Reagent/Re-frame components

Awesome Lists containing this project

README

        

Peanuts Logo

# Peanuts

[![Clojars Project](https://img.shields.io/clojars/v/peanuts.svg)](https://clojars.org/peanuts) [![Build Status](https://travis-ci.org/sansarip/peanuts.svg?branch=main)](https://travis-ci.org/sansarip/peanuts)

> Packing peanuts for decoupling Reagent Form-1 components from Re-frame subscriptions

[deps.edn](https://clojure.org/reference/deps_and_cli)
```clojure
peanuts/peanuts {:mvn/version "0.7.2"}
```

[Leiningen](https://github.com/technomancy/leiningen)
```clojure
[peanuts "0.7.2"]
```

## ToC

1. [Rationale](#rationale)
2. [Usage](#usage)
1. [Interactive devcards](https://sansarip.github.io/peanuts/#!/peanuts.cards.main)
2. [fnc macro](#fnc)
3. [defnc macro](#defnc)
4. [Options](#options)
1. [Redlisting Args](#redlisting-args)
2. [Greenlisting Args](#greenlisting-args)
3. [Known Limitations](#limitations)

## Rationale

This bit is pretty opinionated, but I dislike using the below structure to define Reagent Form-1 components.

```clojure
;; Method A

(defn my-component []
(let [... bunch of re-frame subscriptions here ...]
[:div "hiccup stuff"]))
```

The problem with the above example is that functions defined as such become impure, heavily dependent on the Re-frame subscriptions bound in their let-forms - coupling the components and the subscriptions. This makes it harder to create a library of components that you can share between projects, and it makes the components harder to test.

I prefer something like this...

```clojure
;; Method B

(defn my-component [{:keys [... args ...]}]
[:div "hiccup stuff"])
```

In my preferred method (Method B) the subscriptions would happen outside of the components (in a root component/view), and the data would just simply be passed in. The problem with my preferred method is that it can noticeably affect performance when you have nested components - due to Re-frame subscription mumbo-jumbo.

Enter Peanuts. Peanuts component macros are intended to wrap components implemented like Method B, turning them into components that behave like Method A without the performance drawback. The component will use any args passed in as is _or_ subscribe to them if the args are valid subscription ids/vectors!

I've utilize this simple library in production to great extents, and it has really scratched an itch for me!

## Usage

[![Image from Gyazo](https://i.gyazo.com/5b43a839b7f7f8af7e547eef44efb56c.gif)](https://gyazo.com/5b43a839b7f7f8af7e547eef44efb56c)

*An example of wrapping an existing Form-1 component*

It goes without saying that you should have [re-frame](https://github.com/Day8/re-frame) as a project dependency. You may also need to require it in the namespace(s) you use Peanuts.

The main ways to use peanut components are the `fnc` and `defnc` macros.
For documentation on the older `defc` and `fc` macros, see [the README here](https://github.com/sansarip/peanuts/tree/7b9718519760c254942c2df2eeb5aa52e4ec2181)

#### fnc

Similar to `fn`

```clojure
(ns my-ns
(:require [peanuts.core :refer [defnc]]))

(def foo (fnc [n] [:p (str "Hello, " n "!")]))
```

#### defnc

Similar to `defn`

```clojure
(ns my-ns
(:require [peanuts.core :refer [defnc]]))

(defnc foo [n]
[:p (str "Hello, " n "!")])
```

See this [little blurb](https://cursive-ide.com/userguide/macros.html) if you wish to resolve `defnc` as a `defn` and `fnc` as an `fn` in IntelliJ with Cursive!

You can also import the clj-kondo config like so:

```clojure
{:config-paths ["peanuts/peanuts"]}
```

### Options

Both `fnc` and `defnc` accept an optional map as an argument that can dictate certain options explained below.
In the case of `defnc` the map will also be applied as metadata on the defined name.
You can also pass the options map as the second - or third if there's a docstring - argument.

There are older supported options available that you can [read about here](https://github.com/sansarip/peanuts/tree/5499859a2a00d37454256312b1d784c80ddb6587#options). But, I'm only supporting them for backward compatibility's sake and would advise _against_ using those options!

#### Redlisting Args

The `:redlist` options are there for instances where you'd want certain args that coincide with valid subscription-identifiers/vectors to pass through without being rebound to their respective subscription values.

```clojure
(defnc foo
[adj n]
{:redlist [adj]}
[:p (str "Hello, " adj " " n "!")])

;; Or

(defnc foo
[^:redlist adj n]
[:p (str "Hello, " adj " " n "!")])
```

In the above examples, the `adj` parameter will always be redlisted/excluded from being rebound to subscription values.

You can also redlist args with metadata when calling the function/component.

```clojure
(defnc foo
[adj n]
[:p (str "Hello, " adj " " n "!")])

;; You can also use :rl instead of :redlist
[foo ^:redlist [:my-adj] :my-name]
```

One thing to note with the above example though is that although the `adj` arg will be exempt from being rebound to a subscription value if a valid subscription identifier/vector is passed in, it won't change the amount of code the `defnc` macro emits. In contrast, omitting parameters via the `:redlist` (or `:greenlist`) options does result in less code being emitted - if you care about that!

In addition, you can redlist an arg _once_ with metadata.
This can be handy when you want to pass down a subscription-id/vector to a nested
peanuts component. This is a similar behavior to using the `:redlist` option
in the peanuts component definition.

```clojure
(defnc bar
[adj n]
;; adj will be rebound to subscription value
[:p (str n " is an " adj " name!")])

(defnc foo
[adj n]
[:<>
;; adj will be used as is e.g. [:my-adj]
[:p (str "Hello, " adj " " n "!")]
[bar adj n]])

;; You can also use :rl1 instead of redlist1
[foo ^:redlist1 [:my-adj] :my-name]
```

#### Greenlisting Args

An alternative to the `:redlist` option is the `:greenlist` option. If the `:greenlist` option is specified, then only those specified parameters will be candidates for being rebound subscription values.

```clojure
(defnc foo
[adj n]
{:greenlist [n]}
[:p (str "Hello, " adj " " n "!")])
```

The above example is equivalent to the example in the [Redlisting Args section](#redlisting-args). In the example above, only the greenlisted `n` parameter can be rebound to a subscription value.
One thing to note is that the `:redlist` option always takes precedence over the `:greenlist` option in odd cases where both
options are defined with conflicting args.

## Known Limitations

This library doesn't fully replicate all the bells and whistles of the `defn` macro or the `fn` form.

There are some known limitations:

* Function overloading isn't supported
* `fnc` does not support naming e.g. `(fnc d [])` does not work
* Function constraints e.g.

```clojure
(defn constrained-sqr [x]
{:pre [(pos? x)]
:post [(> % 16), (< % 225)]}
(* x x))
```

It's not that the above limitations can't be fixed; I just haven't run into a necessary use case yet. If there's demand to fix any of the mentioned limitations, I will do it!