npm package: @heartml/reciprocate
Description: Helper utility for adding signal-based reactivity and attribute/property reflection to custom elements
Author: Jared White
Latest version: 1.0.2

# Reciprocate (by the ❤️ Heartml project)

[![npm][npm]][npm-url]

A helper utility for adding signal-based reactivity and attribute/property reflection to any vanilla custom element class. This adheres to the Heartml philosophy of providing "unbundled" features you can add to your own web components-based frontend architecture.

## Installation

```sh
npm i @heartml/reciprocate
```

Reciprocate doesn't include its own signals implementation. We support any implementation which can provide an API adhering to the TC39 proposal. For example, you can use the [Signals library by the Preact team](https://github.com/preactjs/signals/tree/main/packages/core):

```sh
npm i @preact/signals-core
```

```js
import { signal, effect, Signal } from "@preact/signals-core"

Signal.prototype.get = function() {
  return this.value
}
Signal.prototype.set = function(newValue) {
  this.value = newValue
}
```

See the [test fixture here](https://codeberg.org/heartml/reciprocate/src/branch/main/test/fixtures/index.js) for a different installation using [alien-signals](https://github.com/stackblitz/alien-signals).

## Usage

Given an HTML template:

```html
<simple-greeting></simple-greeting>
```

You can write a web component for this custom element which is able to react to attribute and/or property changes:

```js
import { reciprocate } from "@heartml/reciprocate"

export class SimpleGreeting extends HTMLElement {
  /** Attributes you want to watch for signal updates and property reflection */
  static observedAttributes = ["whoa", "mood"]

  static {
    customElements.define("simple-greeting", this)
  }

  /** Used to store one or more rendering effects */
  #effects

  constructor() {
    super()
    this.attachShadow({ mode: "open" })

    /** This is a groovy greeting */
    this.whoa = "Whassup"

    /** How are you feeling today? */
    this.mood = "😃"

    /** Initialize the Reciprocate functionality for this component */
    this.#effects = reciprocate(this, signal, effect)
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `<slot></slot><p id='emoji'></p>`

    /** Create the effects which run whenever signal values are changed */
    this.#effects.run(
      () => (this.textContent = this.whoa),
      () => (this.shadowRoot.querySelector("#emoji").textContent = this.mood)
    )
  }

  disconnectedCallback() {
    this.#effects.stop((fn) => fn())
  }

  attributeChangedCallback(name, _, newValue) {
    this.#effects.setProp(name, newValue)
  }
}
```

In this example, the component will by default render `Whassup` in light DOM, and the 😃 emoji in sides shadow DOM template. However, you can modify this in two distinct ways:

* `<simple-greeting whoa="Howdy" emoji="🤠"></simple-greeting>` — the attributes will be applied to the component instead of the default. You can also edit these attributes in real-time and the component will re-render, and any of these changes will reflect back to the property values in JavaScript.
* `document.querySelector("simple-greeting").whoa = "Yo"` — you can access the properties via JavaScript and set them to new values. When you do so, the new property values will reflect back to the attributes in the DOM.

Since attributes in HTML are always string values, how does Reciprocate handle numbers, booleans, or JSON arrays/objects? By setting your property defaults in `constructor()`, Reciprocate will infer the type:

* `""` is a string
* `false` is a boolean
* `0` is a number
* `[]` is an array
* `{}` is an object

In addition, **camelCase** JS property names will automatically be converted to **kebab-case** HTML attribute names (you can also omit the hyphens if you wish), and visa-versa.

🤔 You may also be wondering, how does Reciprocate even know the existence of properties to covert to signals? It's surprisingly straightforward! When you call the `reciprocate` function, it calls `Object.keys` on the component and iterates on those properties which will be replaced by getters/setters using signals under the hood.

This does mean if you want properties which aren't reflected back to attributes or don't use signals/affect rendering, you'll want to set them _after_ the call:

```js
this.propThatReflects = 123

this.#effects = reciprocate(this, signal, effect)

this.propThatsJSOnly = 456
```

In addition, any custom getters/setters don't affect the Reciprocate setup.

## Zero Boilerplate 😃

It's possible to eliminate even the minor amount of boilerplate demonstrated above for each individual component if you use your own base class:

```js
class BaseElement extends HTMLElement {
  #effects

  constructor() {
    super()
    this.attachShadow({ mode: "open" })
  }

  initialize(fn) {
    fn()
    this.#effects = reciprocate(this, signal, effect)
  }

  render(...fx) {
    this.#effects.run(...fx)
  }

  disconnectedCallback() {
    this.#effects.stop((fn) => fn())
  }

  attributeChangedCallback(name, _, newValue) {
    this.#effects.setProp(name, newValue)
  }
}
```

Given the above, your component class could be streamlined:

```js
export class SimpleGreeting extends BaseElement {
  static observedAttributes = ["whoa", "mood"]

  static {
    customElements.define("simple-greeting", this)
  }

  constructor() {
    super()

    this.initialize(() => {
      this.whoa = "Whassup"
      this.mood = "😃"
    })
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `<slot></slot><p id='emoji'></p>`

    this.render(
      () => (this.textContent = this.whoa),
      () => (this.shadowRoot.querySelector("#emoji").textContent = this.mood)
    )
  }
}
```

## Further Rationale

The `ReciprocalProperty` class takes advantage of fine-grained reactivity using a concept called "signals" to solve a state problem—often encountered in building components (vanilla or otherwise). Signals are values which track "subscriptions" when accessed within an side-effect callback. You write an effect, access one or more signals within that effect, and then any time any of those signal values change, that effect is re-executed.

Here's an example of what we're dealing with when building a "simple" web component:

```html
<my-counter count="1">
  <button type="button" data-behavior="dec">Dec -</button>
  <output>1</output>
  <button type="button" data-behavior="inc">Inc +</button>
</my-counter>
```

It's a typical "counter" example: click the + or - buttons, see the counter update.

In component-speak, we see here that `count` is a prop of the `my-counter` component. In this particular instance it's set to `1`. Even with this very simple example, we run into an immediate conundrum:

* The type of the `count` attribute is not `1`. It's `"1"` (a string). So the simplistic JavaScript code `"1" + 1` doesn't result in `2`, it's `11`. You need to parse the attribute's string value to a number before using it as a property value.
* You need to build getters/setters so `myCounterInstance.count` is available through the component API.
* In most cases, prop mutations should reflect back to the attribute. `myCounterInstance.count = 10` should result in `count="10"` on the HTML attribute. Generally this means serializing values to strings, but in some cases it means removing the attribute. `el.booleanProp = false` shouldn't result in a `booleanprop="false"` attribute but should remove `booleanprop` entirely.
* Handling both attributes and properties with value parsing and reflection isn't the end of it. You also need to avoid an infinite loop (aka setting a property reflects the attribute which then sets the property which then reflects the attribute which then…).
* _And_ to top it all off, you need to be able to observe attribute/prop changes in order to do something—the side effect of the mutation.

Given all this, you're left with two choices:

1. You can build all of this boilerplate yourself and include that custom code in all of your web components. Because obviously trying to write this over and over by hand is a real PITA! (Believe me, I've done it! I know!)
2. You can reach for a helpful web component library which does this all for you. 😎

Unfortunately, the second option typically doesn't mean reaching for a library which _only_ solves these problems, but one which attempts to address a host of other problems (templates, rendering lifecycles, and various other UI component model considerations).

Personally, I tend to like libraries which **do one thing and one thing only—well**. All that Reciprocate does is give you reactive properties by setting up a `ReciprocalProperty` class and corresponding signal for each of your properties. And then, thanks to the Signals library from the folks at Preact or a similar one like it, you can write one or more effects which will re-render (mutate) your component DOM based on those values which can change over time. **Boom. Done**

### Implementing That Counter

Here's an example of using Reciprocate to write that counter component, and we even show how you can skip the initial DOM render if you already expect the component will have been rendered with the appropriate content from the server!

```html
<my-counter count="1">
  <button type="button" data-behavior="dec">Dec -</button>
  <output>1</output>
  <button type="button" data-behavior="inc">Inc +</button>
</my-counter>
```

```js
export class MyCounter extends HTMLElement {
  static observedAttributes = ["count"]

  static {
    customElements.define("my-counter", this)
  }

  #effects
  #resumed = false

  constructor() {
    super()

    this.count = 0

    this.#effects = reciprocate(this, signal, effect)
  }

  connectedCallback() {
    this.addEventListener("click", this)

    /** Create the effects which run whenever signal values are changed */
    this.#effects.run(
      () => {
        const countValue = this.count // set up subscription
        if (this.#resumed) this.querySelector("output").textContent = countValue
      }
    )

    this.#resumed = true
  }

  handleEvent(event) {
    if (event.type === "click") {
      const button = event.target.closest("button")
      switch(button.dataset.behavior) {
        case "dec":
          this.count--;
          break;
        case "inc":
          this.count++;
          break;
      }
    }
  }

  disconnectedCallback() {
    this.#effects.stop((fn) => fn())
  }

  attributeChangedCallback(name, _, newValue) {
    this.#effects.setProp(name, newValue)
  }
}
```

What I love about this example is you can read the code _and immediately understand what is happening_. What ends up happening may feel a bit magical, but there's really no magic at all.

Under the hood, there's a `count` signal which we've initialized with `0`. `this.count++` is effectively `countSignal.set(countSignal.get() + 1)` and `this.count--` is effectively `countSignal.set(countSignal.get() - 1)`. The side effect function we've written will take that value and update the `<output>` element accordingly whenever the `count` attribute/property changes.

Whether the `count` attribute is set/updated through HTML-based APIs, or the `count` prop is set/updated through JS-based APIs, the signal value is always updated accordingly, and that then will trigger your side-effect.

And because you're using Signals, you can take advantage of computed values as well which unlocks a whole new arena of power:

```js
import { computed } from "@preact/signals-core"

// add to the bottom of your `constructor`:
this.times100 = computed(() => this.count * 100 )
```

Now every time the `count` prop mutates, the `this.times100` signal will equal that number times one hundred. And you can access `this.times100.get()` directly in an effect to update UI with that value also!

> [!NOTE]
> If you ever need to access the actual object/function itself that was returned by the `signal` function, just add the `Signal` suffix. For example while `this.count` returns the _value_ of the signal, `this.countSignal` returns _the signal itself_.

You can set up multiple effects to handle different part of your component UI, which is why this approach is termed "fine-grained" reactivity. Instead of a giant `render` method where your component has to supply a template handling all of the data your component supports, you can react to data updates in effects and surgically alter the DOM only when and where needed.

The potential is high for an additional, slightly-more-abstract library to take advantage of this to provide markup-based, declarative bindings between DOM and reactive data.

Hmm… 🤔 (stay tuned!)

----

## Testing

Run `npm run test` to run the test suite. It's built with Mocha, Puppeteer, and a Node static site.

## Contributing

1. Fork it (https://codeberg.org/heartml/reciprocate/fork)
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request

## License

MIT

[npm]: https://img.shields.io/npm/v/@heartml/reciprocate.svg?style=for-the-badge
[npm-url]: https://npmjs.com/package/@crystallized/reciprocate