Ecosyste.ms: Awesome

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

Awesome Lists | Featured Topics | Projects

https://github.com/luavixen/patella

Patella is a library for reactive programming in JavaScript, inspired by Hyperactiv and Vue.js.
https://github.com/luavixen/patella

deno javascript javascript-library reactive reactive-objects reactive-programming reactive-properties reactivity typescript

Last synced: about 2 months ago
JSON representation

Patella is a library for reactive programming in JavaScript, inspired by Hyperactiv and Vue.js.

Awesome Lists containing this project

README

        























# Patella 🔁
Patella, formerly known as Luar, is a library for reactive programming in JavaScript, inspired by [Hyperactiv](https://github.com/elbywan/hyperactiv) and [Vue.js](https://vuejs.org/).
Patella is compatible with Chrome 5, Firefox 4, and Internet Explorer 9.

The [patellar tendon is responsible for the well known "knee-jerk reaction"](https://wikipedia.org/wiki/Patellar_reflex).

Jump to one of:
- [Installation](#installation)
- [Usage](#usage)
- [Examples and snippets](#examples-and-snippets)
- [Pitfalls](#pitfalls)
- [API](#api)
- [Authors](#authors)
- [License](#license)

## Installation
Patella is available via [npm](https://www.npmjs.com/package/patella):
```sh
$ npm install patella
```
```javascript
// ECMAScript module environments
import { observe, ignore, computed, dispose } from "patella";
// CommonJS environments
const { observe, ignore, computed, dispose } = require("patella");
```

Or, for people working without a bundler, it can be included from [UNPKG](https://www.unpkg.com/browse/patella@latest/):
```html

Patella.observe({});
Patella.ignore({});
Patella.computed(() => {});
Patella.dispose(() => {});

```

Various other Patella builds are available in the [dist](./dist) folder, including sourcemaps and minified versions.
Minification is performed using both [Terser](https://github.com/terser/terser) and [UglifyJS](https://github.com/mishoo/UglifyJS) using custom configurations designed for a balance of speed and size (Patella is a micro-library at 900~ bytes gzipped).

## Usage
Patella provides functions for observing object mutations and acting on those mutations automatically.
Possibly the best way to learn is by example, so let's take a page out of [Vue.js's guide](https://vuejs.org/v2/guide/events.html) and make a button that counts how many times it has been clicked using Patella's `observe(object)` and `computed(func)`:
```html

Click Counter

const $button = document.getElementsByTagName("button")[0];
const model = Patella.observe({
clicks: 0
});
Patella.computed(() => {
$button.innerText = model.clicks
? `I've been clicked ${model.clicks} times`
: "Click me!";
});

```
![](./examples/counter-vid.gif)

View the [full source](./examples/counter.html) or [try it on JSFiddle](https://jsfiddle.net/luawtf/hL6g4emk/latest).

Notice how in the above example, the `` doesn't do any extra magic to change its text when clicked; it just increments the model's click counter, which is "connected" to the button's text in the computed function.

Now let's try doing some math, here's a snippet that adds and multiplies two numbers:
```javascript
const calculator = Patella.observe({
left: 1,
right: 1,
sum: 0,
product: 0
});

// Connect left, right -> sum
Patella.computed(() => calculator.sum = calculator.left + calculator.right);
// Connect left, right -> product
Patella.computed(() => calculator.product = calculator.left * calculator.right);

calculator.left = 2;
calculator.right = 10;
console.log(calculator.sum, calculator.product); // Output: 12 20

calcuator.left = 3;
console.log(calculator.sum, calculator.product); // Output: 13 30
```
Pretty cool, right?
Patella's main goal is to be as simple as possible; you only need two functions to build almost anything.

## Examples and snippets
Jump to one of:
- [Concatenator](#concatenator)
- [Debounced search](#debounced-search)
- [Pony browser](#pony-browser)
- [Multiple objects snippet](#multiple-objects-snippet)
- [Linked computed functions snippet](#linked-computed-functions-snippet)

### Concatenator
```html

Concatenator

const $output = document.getElementById("output");
const model = Patella.observe({
first: "",
second: "",
full: ""
});
Patella.computed(() => {
model.full = model.first + " " + model.second;
});
Patella.computed(() => {
$output.innerText = model.full;
});

```
![](./examples/concatenator-vid.gif)

View the [full source](./examples/concatenator.html) or [try it on JSFiddle](https://jsfiddle.net/luawtf/zvnm4jp7/latest).

### Debounced search
```html

Debounced Search

const $search = document.getElementById("search");

const model = Patella.observe({
input: "",
search: ""
});

Patella.computed(() => {
search.innerText = model.search;
});

let timeoutID;
Patella.computed(() => {
const input = model.input;
if (timeoutID) clearTimeout(timeoutID);
timeoutID = setTimeout(() => {
model.search = input;
}, 1000);
});

```
![](./examples/debounce-vid.gif)

View the [full source](./examples/debounce.html) or [try it on JSFiddle](https://jsfiddle.net/luawtf/abd3qxft/latest).

### Pony browser
```html

Pony Browser




    // Find elements
    const $app = document.getElementById("app");
    const [, $select, $list, $input] = $app.children;

    // Declare model
    const model = Patella.observe({
    /* Truncated, find full source in ./examples/pony.html */
    });

    // Populate <select>
    for (const [value, { name }] of Object.entries(model.characterSets)) {
    const $option = document.createElement("option");
    $option.value = value;
    $option.innerText = name;
    $select.appendChild($option);
    }

    // Connect model.selected.key -> model.selected.current
    Patella.computed(() => {
    model.selected.current = model.characterSets[model.selected.key];
    });

    // Connect model.selected.current.members -> <ul>
    Patella.computed(() => {
    $list.innerHTML = "";
    for (const member of model.selected.current.members) {
    const $entry = document.createElement("li");
    $entry.innerText = member;
    $list.appendChild($entry);
    }
    });

    // Connect <select> -> model.selected.key
    $select.addEventListener("change", () => {
    model.selected.key = $select.value;
    });

    // Connect <input> -> model.selected.current.members
    $input.addEventListener("keyup", ({ key }) => {
    if (key !== "Enter") return;

    const currentSet = model.selected.current;
    currentSet.members = [
    ...currentSet.members,
    $input.value
    ];

    $input.value = "";
    });

    ```
    ![](./examples/pony-vid.gif)

    View the [full source](./examples/pony.html) or [try it on JSFiddle](https://jsfiddle.net/luawtf/84wmaz0g/latest).

    ## Multiple objects snippet
    ```javascript
    // Setting up some reactive objects that contain some data about a US president...
    // Disclaimer: I am not an American :P
    const person = Patella.observe({
    name: { first: "George", last: "Washington" },
    age: 288
    });
    const account = Patella.observe({
    user: "big-george12",
    password: "IHateTheQueen!1"
    });

    // Declare that we will output a log message whenever person.name.first, account.user, or person.age are updated
    Patella.computed(() => console.log(
    `${person.name.first}'s username is ${account.user} (${person.age} years old)`
    )); // Output: George's username is big-george12 (288 years old)

    // Changing reactive properties will only run computed functions that depend on them
    account.password = "not-telling"; // Does not output (no computed function depends on this)

    // All operators work when updating properties
    account.user += "3"; // Output: George's username is big-george123 (288 years old)
    person.age++; // Output: George's username is big-george123 (289 years old)

    // You can even replace objects entirely
    // This will automatically observe this new object and will still trigger dependant computed functions
    // Note: You should ideally use ignore or dispose to prevent depending on objects that get replaced, see pitfalls
    person.name = {
    first: "Abraham",
    last: "Lincoln"
    }; // Output: Abraham's username is big-george123 (289 years old)

    person.name.first = "Thomas"; // Output: Thomas's username is big-george123 (289 years old)
    ```

    ### Linked computed functions snippet
    ```javascript
    // Create our nums object, with some default values for properties that will be computed
    const nums = Patella.observe({
    a: 33, b: 23, c: 84,
    x: 0,
    sumAB: 0, sumAX: 0, sumCX: 0,
    sumAllSums: 0
    });

    // Declare that (x) will be equal to (a + b + c)
    Patella.computed(() => nums.x = nums.a + nums.b + nums.c);
    // Declare that (sumAB) will be equal to (a + b)
    Patella.computed(() => nums.sumAB = nums.a + nums.b);
    // Declare that (sumAX) will be equal to (a + x)
    Patella.computed(() => nums.sumAX = nums.a + nums.x);
    // Declare that (sumCX) will be equal to (c + x)
    Patella.computed(() => nums.sumCX = nums.c + nums.x);
    // Declare that (sumAllSums) will be equal to (sumAB + sumAX + sumCX)
    Patella.computed(() => nums.sumAllSums = nums.sumAB + nums.sumAX + nums.sumCX);

    // Now lets check the (sumAllSums) value
    console.log(nums.sumAllSums); // Output: 453

    // Notice that when we update one value ...
    nums.c += 2;
    // ... all the other values update! (since we declared them as such)
    console.log(nums.sumAllSums); // Output: 459
    ```

    ## Pitfalls
    Patella uses JavaScript's [getters](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Functions/get)[ and ](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)[setters](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Functions/set) to make all the reactivity magic possible, which comes with some tradeoffs that other libraries like [Hyperactiv](https://github.com/elbywan/hyperactiv) (which uses [Proxy](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy)) don't have to deal with.
    This section details some of the stuff to look out for when using Patella in your applications.

    ### Computed functions can cause infinite loops
    ```javascript
    const object = Patella.observe({ x: 10, y: 20 });

    Patella.computed(function one() {
    if (object.x > 20) object.y++;
    });

    Patella.computed(function two() {
    if (object.y > 20) object.x++;
    });

    object.x = 25;
    // Uncaught Error: Computed queue overflow! Last 10 functions in the queue:
    // 1993: one
    // 1994: two
    // 1995: one
    // 1996: two
    // 1997: one
    // 1998: two
    // 1999: one
    // 2000: two
    // 2001: one
    // 2002: two
    // 2003: one
    ```

    ### Array mutations do not trigger dependencies
    ```javascript
    const object = Patella.observe({
    array: [1, 2, 3]
    });

    Patella.computed(() => console.log(object.array)); // Output: 1,2,3

    object.array[2] = 4; // No output, arrays are not reactive!
    object.array.push(5); // Still no output, as Patella does not replace array methods

    // If you want to use arrays, do it like this:
    // 1. Run your operations
    object.array[2] = 3;
    object.array[3] = 4;
    object.array.push(5);
    // 2. Then set the array to itself
    object.array = object.array; // Output: 1,2,3,4,5
    ```

    ### Properties added after observation are not reactive
    ```javascript
    const object = Patella.observe({ x: 10 });
    object.y = 20;

    Patella.computed(() => console.log(object.x)); // Output: 10
    Patella.computed(() => console.log(object.y)); // Output: 20

    object.x += 2; // Output: 12

    object.y += 2; // No output, as this property was added after observation

    Patella.observe(object);

    object.y += 2; // Still no output, as objects cannot be re-observed
    ```

    ### Prototypes will not be made reactive unless explicitly observed
    ```javascript
    const object = { a: 20 };
    const prototype = { b: 10 };
    Object.setPrototypeOf(object, prototype);

    Patella.observe(object);

    Patella.computed(() => console.log(object.a)); // Output: 10
    Patella.computed(() => console.log(object.b)); // Output: 20

    object.a = 15; // Output: 15

    object.b = 30; // No output, as this isn't an actual property on the object
    prototype.b = 36; // No output, as prototypes are not made reactive by observe

    Patella.observe(prototype);

    prototype.b = 32; // Output: 32
    ```

    ### Non-enumerable and non-configurable properties will not be made reactive
    ```javascript
    const object = { x: 1 };
    Object.defineProperty(object, "y", {
    configurable: true,
    enumerable: false,
    value: 2
    });
    Object.defineProperty(object, "z", {
    configurable: false,
    enumerable: true,
    value: 3
    });

    Patella.observe(object);

    Patella.computed(() => console.log(object.x)); // Output: 1
    Patella.computed(() => console.log(object.y)); // Output: 2
    Patella.computed(() => console.log(object.z)); // Output: 3

    object.x--; // Output: 0

    object.y--; // No output as this property is non-enumerable
    object.z--; // No output as this property is non-configurable
    ```

    ### Enumerable and configurable but non-writable properties will be made writable
    ```javascript
    const object = {};
    Object.defineProperty(object, "val", {
    configurable: true,
    enumerable: true,
    writable: false,
    value: 10
    });

    object.val = 20; // Does nothing
    console.log(object.val); // Output: 10

    Patella.observe(object);

    object.val = 20; // Works because the property descriptor has been overwritten
    console.log(object.val); // Output: 20
    ```

    ### Getter/setter properties will be accessed then lose their getter/setters
    ```javascript
    const object = {
    get val() {
    console.log("Gotten!");
    return 10;
    }
    };

    object.val; // Output: Gotten!

    Patella.observe(object); // Output: Gotten!

    object.val; // No output as the getter has been overwritten
    ```

    ### Properties named `__proto__` are ignored
    ```javascript
    const object = {};
    Object.defineProperty(object, "__proto__", {
    configurable: true,
    enumerable: true,
    writable: true,
    value: 10
    });

    Patella.observe(object);

    Patella.computed(() => console.log(object.__proto__)); // Output: 10

    object.__proto__++; // No output as properties named __proto__ are ignored
    ```

    ## API

    function observe(object)


    Description:


    • Makes an object and its properties reactive recursively.
      Subobjects (but not subfunctions!) will also be observed.
      Note that observe does not create a new object, it mutates the object passed into it: observe(object) === object.


    Parameters:


    • object — Object or function to make reactive


    Returns:

    • Input object, now reactive

    function ignore(object)


    Description:


    • Prevents an object from being made reactive, observe will do nothing.
      Note that ignore is not recursive, so subobjects can still be made reactive by calling observe on them directly.


    Parameters:


    • object — Object or function to ignore


    Returns:

    • Input object, now permanently ignored

    function computed(func)


    Description:


    • Calls func with no arguments and records a list of all the reactive properties it accesses.
      func will then be called again whenever any of the accessed properties are mutated.
      Note that if func has been disposed with !!clean === false, no operation will be performed.


    Parameters:


    • func — Function to execute


    Returns:

    • Input func

    function dispose(func, clean)


    Description:


    • "Disposes" a function that was run with computed, deregistering it so that it will no longer be called whenever any of its accessed reactive properties update.
      The clean parameter controls whether calling computed with func will work or no-op.


    Parameters:


    • func — Function to dispose, omit to dispose the currently executing computed function


    • clean — If truthy, only deregister the function from all dependencies, but allow it to be used with computed again in the future


    Returns:

    • Input func if func is valid, otherwise undefined

    ## Authors
    Made with ❤ by Lua MacDougall ([foxgirl.dev](https://foxgirl.dev/))

    ## License
    This project is licensed under [MIT](LICENSE).
    More info in the [LICENSE](LICENSE) file.

    "A short, permissive software license. Basically, you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source. There are many variations of this license in use." - [tl;drLegal](https://tldrlegal.com/license/mit-license)