Patella 🔁
Patella, formerly known as Luar, is a library for reactive programming in JavaScript, inspired by Hyperactiv and Vue.js.
Patella is compatible with Chrome 5, Firefox 4, and Internet Explorer 9.
The patellar tendon is responsible for the well known "knee-jerk reaction".
Jump to one of:
Installation
Patella is available via npm:
$ npm install patella
import { observe, ignore, computed, dispose } from "patella";
const { observe, ignore, computed, dispose } = require("patella");
Or, for people working without a bundler, it can be included from UNPKG:
<script src="https://www.unpkg.com/patella"></script>
<script>
Patella.observe({});
Patella.ignore({});
Patella.computed(() => {});
Patella.dispose(() => {});
</script>
Various other Patella builds are available in the dist folder, including sourcemaps and minified versions.
Minification is performed using both Terser and 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 and make a button that counts how many times it has been clicked using Patella's observe(object)
and computed(func)
:
<h1>Click Counter</h1>
<button onclick="model.clicks++"></button>
<script>
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!";
});
</script>

View the full source or try it on JSFiddle.
Notice how in the above example, the <button>
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:
const calculator = Patella.observe({
left: 1,
right: 1,
sum: 0,
product: 0
});
Patella.computed(() => calculator.sum = calculator.left + calculator.right);
Patella.computed(() => calculator.product = calculator.left * calculator.right);
calculator.left = 2;
calculator.right = 10;
console.log(calculator.sum, calculator.product);
calcuator.left = 3;
console.log(calculator.sum, calculator.product);
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
<h1>Concatenator</h1>
<input type="text" oninput="model.first = value" placeholder="Enter some"/>
<input type="text" oninput="model.second = value" placeholder="text!"/>
<h3 id="output"></h3>
<script>
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;
});
</script>

View the full source or try it on JSFiddle.
Debounced search
<h1>Debounced Search</h1>
<input type="text" oninput="model.input = value" placeholder="Enter your debounced search"/>
<h3 id="search"></h3>
<script>
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);
});
</script>

View the full source or try it on JSFiddle.
Pony browser
<main id="app">
<h1>Pony Browser</h1>
<select></select>
<ul></ul>
<input type="text" placeholder="Add another pony"/>
</main>
<script>
const $app = document.getElementById("app");
const [, $select, $list, $input] = $app.children;
const model = Patella.observe({
});
for (const [value, { name }] of Object.entries(model.characterSets)) {
const $option = document.createElement("option");
$option.value = value;
$option.innerText = name;
$select.appendChild($option);
}
Patella.computed(() => {
model.selected.current = model.characterSets[model.selected.key];
});
Patella.computed(() => {
$list.innerHTML = "";
for (const member of model.selected.current.members) {
const $entry = document.createElement("li");
$entry.innerText = member;
$list.appendChild($entry);
}
});
$select.addEventListener("change", () => {
model.selected.key = $select.value;
});
$input.addEventListener("keyup", ({ key }) => {
if (key !== "Enter") return;
const currentSet = model.selected.current;
currentSet.members = [
...currentSet.members,
$input.value
];
$input.value = "";
});
</script>

View the full source or try it on JSFiddle.
Multiple objects snippet
const person = Patella.observe({
name: { first: "George", last: "Washington" },
age: 288
});
const account = Patella.observe({
user: "big-george12",
password: "IHateTheQueen!1"
});
Patella.computed(() => console.log(
`${person.name.first}'s username is ${account.user} (${person.age} years old)`
));
account.password = "not-telling";
account.user += "3";
person.age++;
person.name = {
first: "Abraham",
last: "Lincoln"
};
person.name.first = "Thomas";
Linked computed functions snippet
const nums = Patella.observe({
a: 33, b: 23, c: 84,
x: 0,
sumAB: 0, sumAX: 0, sumCX: 0,
sumAllSums: 0
});
Patella.computed(() => nums.x = nums.a + nums.b + nums.c);
Patella.computed(() => nums.sumAB = nums.a + nums.b);
Patella.computed(() => nums.sumAX = nums.a + nums.x);
Patella.computed(() => nums.sumCX = nums.c + nums.x);
Patella.computed(() => nums.sumAllSums = nums.sumAB + nums.sumAX + nums.sumCX);
console.log(nums.sumAllSums);
nums.c += 2;
console.log(nums.sumAllSums);
Pitfalls
Patella uses JavaScript's getters and setters to make all the reactivity magic possible, which comes with some tradeoffs that other libraries like Hyperactiv (which uses 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
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;
Array mutations do not trigger dependencies
const object = Patella.observe({
array: [1, 2, 3]
});
Patella.computed(() => console.log(object.array));
object.array[2] = 4;
object.array.push(5);
object.array[2] = 3;
object.array[3] = 4;
object.array.push(5);
object.array = object.array;
Properties added after observation are not reactive
const object = Patella.observe({ x: 10 });
object.y = 20;
Patella.computed(() => console.log(object.x));
Patella.computed(() => console.log(object.y));
object.x += 2;
object.y += 2;
Patella.observe(object);
object.y += 2;
Prototypes will not be made reactive unless explicitly observed
const object = { a: 20 };
const prototype = { b: 10 };
Object.setPrototypeOf(object, prototype);
Patella.observe(object);
Patella.computed(() => console.log(object.a));
Patella.computed(() => console.log(object.b));
object.a = 15;
object.b = 30;
prototype.b = 36;
Patella.observe(prototype);
prototype.b = 32;
Non-enumerable and non-configurable properties will not be made reactive
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));
Patella.computed(() => console.log(object.y));
Patella.computed(() => console.log(object.z));
object.x--;
object.y--;
object.z--;
Enumerable and configurable but non-writable properties will be made writable
const object = {};
Object.defineProperty(object, "val", {
configurable: true,
enumerable: true,
writable: false,
value: 10
});
object.val = 20;
console.log(object.val);
Patella.observe(object);
object.val = 20;
console.log(object.val);
Getter/setter properties will be accessed then lose their getter/setters
const object = {
get val() {
console.log("Gotten!");
return 10;
}
};
object.val;
Patella.observe(object);
object.val;
Properties named __proto__
are ignored
const object = {};
Object.defineProperty(object, "__proto__", {
configurable: true,
enumerable: true,
writable: true,
value: 10
});
Patella.observe(object);
Patella.computed(() => console.log(object.__proto__));
object.__proto__++;
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 dispose
d with !!clean === false
, no operation will be performed.
Parameters:
func
— Function to execute
Returns:
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)
License
This project is licensed under MIT.
More info in the 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