UNPKG

18.2 kBMarkdownView Raw
1<p>
2 <a href="https://www.npmjs.com/package/patella">
3 <img src="https://badgen.net/npm/v/patella?style=flat-square">
4 </a>
5 <a href="https://www.npmjs.com/package/patella?activeTab=dependencies">
6 <img src="https://badgen.net/bundlephobia/dependency-count/patella?style=flat-square">
7 </a>
8 <a href="https://github.com/luavixen/Patella/blob/master/lib/patella.d.ts">
9 <img src="https://badgen.net/npm/types/patella?style=flat-square">
10 </a>
11 <a href="https://bundlephobia.com/result?p=patella">
12 <img src="https://badgen.net/bundlephobia/minzip/patella?style=flat-square">
13 </a>
14 <a href="https://github.com/luavixen/Patella/actions">
15 <img src="https://badgen.net/github/status/luavixen/Patella/master?label=build&style=flat-square">
16 </a>
17 <a href="https://coveralls.io/github/luavixen/Patella">
18 <img src="https://badgen.net/coveralls/c/github/luavixen/Patella?style=flat-square">
19 </a>
20 <a href="https://github.com/luavixen/Patella/blob/master/LICENSE">
21 <img src="https://badgen.net/github/license/luavixen/Patella?style=flat-square">
22 </a>
23</p>
24
25# Patella &#x1F501;
26Patella, formerly known as Luar, is a library for <a href="https://wikipedia.org/wiki/Reactive_programming">reactive programming</a> in JavaScript, inspired by [Hyperactiv](https://github.com/elbywan/hyperactiv) and [Vue.js](https://vuejs.org/).
27Patella is compatible with Chrome 5, Firefox 4, and Internet Explorer 9.
28
29The [patellar tendon is responsible for the well known "knee-jerk reaction"](https://wikipedia.org/wiki/Patellar_reflex).
30
31Jump to one of:
32 - [Installation](#installation)
33 - [Usage](#usage)
34 - [Examples and snippets](#examples-and-snippets)
35 - [Pitfalls](#pitfalls)
36 - [API](#api)
37 - [Authors](#authors)
38 - [License](#license)
39
40## Installation
41Patella is available via [npm](https://www.npmjs.com/package/patella):
42```sh
43$ npm install patella
44```
45```javascript
46// ECMAScript module environments
47import { observe, ignore, computed, dispose } from "patella";
48// CommonJS environments
49const { observe, ignore, computed, dispose } = require("patella");
50```
51
52Or, for people working without a bundler, it can be included from [UNPKG](https://www.unpkg.com/browse/patella@latest/):
53```html
54<script src="https://www.unpkg.com/patella"></script>
55<script>
56 Patella.observe({});
57 Patella.ignore({});
58 Patella.computed(() => {});
59 Patella.dispose(() => {});
60</script>
61```
62
63Various other Patella builds are available in the [dist](./dist) folder, including sourcemaps and minified versions.
64Minification 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).
65
66## Usage
67Patella provides functions for observing object mutations and acting on those mutations automatically.
68Possibly 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)`:
69```html
70<h1>Click Counter</h1>
71<button onclick="model.clicks++"></button>
72<script>
73 const $button = document.getElementsByTagName("button")[0];
74 const model = Patella.observe({
75 clicks: 0
76 });
77 Patella.computed(() => {
78 $button.innerText = model.clicks
79 ? `I've been clicked ${model.clicks} times`
80 : "Click me!";
81 });
82</script>
83```
84![](./examples/counter-vid.gif)<br>
85View the [full source](./examples/counter.html) or [try it on JSFiddle](https://jsfiddle.net/luawtf/hL6g4emk/latest).
86
87Notice 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.
88
89Now let's try doing some math, here's a snippet that adds and multiplies two numbers:
90```javascript
91const calculator = Patella.observe({
92 left: 1,
93 right: 1,
94 sum: 0,
95 product: 0
96});
97
98// Connect left, right -> sum
99Patella.computed(() => calculator.sum = calculator.left + calculator.right);
100// Connect left, right -> product
101Patella.computed(() => calculator.product = calculator.left * calculator.right);
102
103calculator.left = 2;
104calculator.right = 10;
105console.log(calculator.sum, calculator.product); // Output: 12 20
106
107calcuator.left = 3;
108console.log(calculator.sum, calculator.product); // Output: 13 30
109```
110Pretty cool, right?
111Patella's main goal is to be as simple as possible; you only need two functions to build almost anything.
112
113## Examples and snippets
114Jump to one of:
115 - [Concatenator](#concatenator)
116 - [Debounced search](#debounced-search)
117 - [Pony browser](#pony-browser)
118 - [Multiple objects snippet](#multiple-objects-snippet)
119 - [Linked computed functions snippet](#linked-computed-functions-snippet)
120
121### Concatenator
122```html
123<h1>Concatenator</h1>
124<input type="text" oninput="model.first = value" placeholder="Enter some"/>
125<input type="text" oninput="model.second = value" placeholder="text!"/>
126<h3 id="output"></h3>
127<script>
128 const $output = document.getElementById("output");
129 const model = Patella.observe({
130 first: "",
131 second: "",
132 full: ""
133 });
134 Patella.computed(() => {
135 model.full = model.first + " " + model.second;
136 });
137 Patella.computed(() => {
138 $output.innerText = model.full;
139 });
140</script>
141```
142![](./examples/concatenator-vid.gif)<br>
143View the [full source](./examples/concatenator.html) or [try it on JSFiddle](https://jsfiddle.net/luawtf/zvnm4jp7/latest).
144
145### Debounced search
146```html
147<h1>Debounced Search</h1>
148<input type="text" oninput="model.input = value" placeholder="Enter your debounced search"/>
149<h3 id="search"></h3>
150<script>
151 const $search = document.getElementById("search");
152
153 const model = Patella.observe({
154 input: "",
155 search: ""
156 });
157
158 Patella.computed(() => {
159 search.innerText = model.search;
160 });
161
162 let timeoutID;
163 Patella.computed(() => {
164 const input = model.input;
165 if (timeoutID) clearTimeout(timeoutID);
166 timeoutID = setTimeout(() => {
167 model.search = input;
168 }, 1000);
169 });
170</script>
171```
172![](./examples/debounce-vid.gif)<br>
173View the [full source](./examples/debounce.html) or [try it on JSFiddle](https://jsfiddle.net/luawtf/abd3qxft/latest).
174
175### Pony browser
176```html
177<main id="app">
178 <h1>Pony Browser</h1>
179 <select></select>
180 <ul></ul>
181 <input type="text" placeholder="Add another pony"/>
182</main>
183<script>
184 // Find elements
185 const $app = document.getElementById("app");
186 const [, $select, $list, $input] = $app.children;
187
188 // Declare model
189 const model = Patella.observe({
190 /* Truncated, find full source in ./examples/pony.html */
191 });
192
193 // Populate <select>
194 for (const [value, { name }] of Object.entries(model.characterSets)) {
195 const $option = document.createElement("option");
196 $option.value = value;
197 $option.innerText = name;
198 $select.appendChild($option);
199 }
200
201 // Connect model.selected.key -> model.selected.current
202 Patella.computed(() => {
203 model.selected.current = model.characterSets[model.selected.key];
204 });
205
206 // Connect model.selected.current.members -> <ul>
207 Patella.computed(() => {
208 $list.innerHTML = "";
209 for (const member of model.selected.current.members) {
210 const $entry = document.createElement("li");
211 $entry.innerText = member;
212 $list.appendChild($entry);
213 }
214 });
215
216 // Connect <select> -> model.selected.key
217 $select.addEventListener("change", () => {
218 model.selected.key = $select.value;
219 });
220
221 // Connect <input> -> model.selected.current.members
222 $input.addEventListener("keyup", ({ key }) => {
223 if (key !== "Enter") return;
224
225 const currentSet = model.selected.current;
226 currentSet.members = [
227 ...currentSet.members,
228 $input.value
229 ];
230
231 $input.value = "";
232 });
233</script>
234```
235![](./examples/pony-vid.gif)<br>
236View the [full source](./examples/pony.html) or [try it on JSFiddle](https://jsfiddle.net/luawtf/84wmaz0g/latest).
237
238## Multiple objects snippet
239```javascript
240// Setting up some reactive objects that contain some data about a US president...
241// Disclaimer: I am not an American :P
242const person = Patella.observe({
243 name: { first: "George", last: "Washington" },
244 age: 288
245});
246const account = Patella.observe({
247 user: "big-george12",
248 password: "IHateTheQueen!1"
249});
250
251// Declare that we will output a log message whenever person.name.first, account.user, or person.age are updated
252Patella.computed(() => console.log(
253 `${person.name.first}'s username is ${account.user} (${person.age} years old)`
254)); // Output: George's username is big-george12 (288 years old)
255
256// Changing reactive properties will only run computed functions that depend on them
257account.password = "not-telling"; // Does not output (no computed function depends on this)
258
259// All operators work when updating properties
260account.user += "3"; // Output: George's username is big-george123 (288 years old)
261person.age++; // Output: George's username is big-george123 (289 years old)
262
263// You can even replace objects entirely
264// This will automatically observe this new object and will still trigger dependant computed functions
265// Note: You should ideally use ignore or dispose to prevent depending on objects that get replaced, see pitfalls
266person.name = {
267 first: "Abraham",
268 last: "Lincoln"
269}; // Output: Abraham's username is big-george123 (289 years old)
270
271person.name.first = "Thomas"; // Output: Thomas's username is big-george123 (289 years old)
272```
273
274### Linked computed functions snippet
275```javascript
276// Create our nums object, with some default values for properties that will be computed
277const nums = Patella.observe({
278 a: 33, b: 23, c: 84,
279 x: 0,
280 sumAB: 0, sumAX: 0, sumCX: 0,
281 sumAllSums: 0
282});
283
284// Declare that (x) will be equal to (a + b + c)
285Patella.computed(() => nums.x = nums.a + nums.b + nums.c);
286// Declare that (sumAB) will be equal to (a + b)
287Patella.computed(() => nums.sumAB = nums.a + nums.b);
288// Declare that (sumAX) will be equal to (a + x)
289Patella.computed(() => nums.sumAX = nums.a + nums.x);
290// Declare that (sumCX) will be equal to (c + x)
291Patella.computed(() => nums.sumCX = nums.c + nums.x);
292// Declare that (sumAllSums) will be equal to (sumAB + sumAX + sumCX)
293Patella.computed(() => nums.sumAllSums = nums.sumAB + nums.sumAX + nums.sumCX);
294
295// Now lets check the (sumAllSums) value
296console.log(nums.sumAllSums); // Output: 453
297
298// Notice that when we update one value ...
299nums.c += 2;
300// ... all the other values update! (since we declared them as such)
301console.log(nums.sumAllSums); // Output: 459
302```
303
304## Pitfalls
305Patella 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.
306This section details some of the stuff to look out for when using Patella in your applications.
307
308### Computed functions can cause infinite loops
309```javascript
310const object = Patella.observe({ x: 10, y: 20 });
311
312Patella.computed(function one() {
313 if (object.x > 20) object.y++;
314});
315
316Patella.computed(function two() {
317 if (object.y > 20) object.x++;
318});
319
320object.x = 25;
321// Uncaught Error: Computed queue overflow! Last 10 functions in the queue:
322// 1993: one
323// 1994: two
324// 1995: one
325// 1996: two
326// 1997: one
327// 1998: two
328// 1999: one
329// 2000: two
330// 2001: one
331// 2002: two
332// 2003: one
333```
334
335### Array mutations do not trigger dependencies
336```javascript
337const object = Patella.observe({
338 array: [1, 2, 3]
339});
340
341Patella.computed(() => console.log(object.array)); // Output: 1,2,3
342
343object.array[2] = 4; // No output, arrays are not reactive!
344object.array.push(5); // Still no output, as Patella does not replace array methods
345
346// If you want to use arrays, do it like this:
347// 1. Run your operations
348object.array[2] = 3;
349object.array[3] = 4;
350object.array.push(5);
351// 2. Then set the array to itself
352object.array = object.array; // Output: 1,2,3,4,5
353```
354
355### Properties added after observation are not reactive
356```javascript
357const object = Patella.observe({ x: 10 });
358object.y = 20;
359
360Patella.computed(() => console.log(object.x)); // Output: 10
361Patella.computed(() => console.log(object.y)); // Output: 20
362
363object.x += 2; // Output: 12
364
365object.y += 2; // No output, as this property was added after observation
366
367Patella.observe(object);
368
369object.y += 2; // Still no output, as objects cannot be re-observed
370```
371
372### Prototypes will not be made reactive unless explicitly observed
373```javascript
374const object = { a: 20 };
375const prototype = { b: 10 };
376Object.setPrototypeOf(object, prototype);
377
378Patella.observe(object);
379
380Patella.computed(() => console.log(object.a)); // Output: 10
381Patella.computed(() => console.log(object.b)); // Output: 20
382
383object.a = 15; // Output: 15
384
385object.b = 30; // No output, as this isn't an actual property on the object
386prototype.b = 36; // No output, as prototypes are not made reactive by observe
387
388Patella.observe(prototype);
389
390prototype.b = 32; // Output: 32
391```
392
393### Non-enumerable and non-configurable properties will not be made reactive
394```javascript
395const object = { x: 1 };
396Object.defineProperty(object, "y", {
397 configurable: true,
398 enumerable: false,
399 value: 2
400});
401Object.defineProperty(object, "z", {
402 configurable: false,
403 enumerable: true,
404 value: 3
405});
406
407Patella.observe(object);
408
409Patella.computed(() => console.log(object.x)); // Output: 1
410Patella.computed(() => console.log(object.y)); // Output: 2
411Patella.computed(() => console.log(object.z)); // Output: 3
412
413object.x--; // Output: 0
414
415object.y--; // No output as this property is non-enumerable
416object.z--; // No output as this property is non-configurable
417```
418
419### Enumerable and configurable but non-writable properties will be made writable
420```javascript
421const object = {};
422Object.defineProperty(object, "val", {
423 configurable: true,
424 enumerable: true,
425 writable: false,
426 value: 10
427});
428
429object.val = 20; // Does nothing
430console.log(object.val); // Output: 10
431
432Patella.observe(object);
433
434object.val = 20; // Works because the property descriptor has been overwritten
435console.log(object.val); // Output: 20
436```
437
438### Getter/setter properties will be accessed then lose their getter/setters
439```javascript
440const object = {
441 get val() {
442 console.log("Gotten!");
443 return 10;
444 }
445};
446
447object.val; // Output: Gotten!
448
449Patella.observe(object); // Output: Gotten!
450
451object.val; // No output as the getter has been overwritten
452```
453
454### Properties named `__proto__` are ignored
455```javascript
456const object = {};
457Object.defineProperty(object, "__proto__", {
458 configurable: true,
459 enumerable: true,
460 writable: true,
461 value: 10
462});
463
464Patella.observe(object);
465
466Patella.computed(() => console.log(object.__proto__)); // Output: 10
467
468object.__proto__++; // No output as properties named __proto__ are ignored
469```
470
471## API
472<h4 id="observe"><code>function observe(object)</code></h4>
473Description:
474<ul>
475 <li>
476 Makes an object and its properties reactive recursively.
477 Subobjects (but not subfunctions!) will also be observed.
478 Note that <code>observe</code> does not create a new object, it mutates the object passed into it: <code>observe(object) === object</code>.
479 </li>
480</ul>
481Parameters:
482<ul>
483 <li><code>object</code> &mdash; Object or function to make reactive</li>
484</ul>
485Returns:
486<ul>
487 <li>Input <code>object</code>, now reactive</li>
488</ul>
489
490<h4 id="ignore"><code>function ignore(object)</code></h4>
491Description:
492<ul>
493 <li>
494 Prevents an object from being made reactive, <code>observe</code> will do nothing.
495 Note that <code>ignore</code> is not recursive, so subobjects can still be made reactive by calling <code>observe</code> on them directly.
496 </li>
497</ul>
498Parameters:
499<ul>
500 <li><code>object</code> &mdash; Object or function to ignore</li>
501</ul>
502Returns:
503<ul>
504 <li>Input <code>object</code>, now permanently ignored</li>
505</ul>
506
507<h4 id="computed"><code>function computed(func)</code></h4>
508Description:
509<ul>
510 <li>
511 Calls <code>func</code> with no arguments and records a list of all the reactive properties it accesses.
512 <code>func</code> will then be called again whenever any of the accessed properties are mutated.
513 Note that if <code>func</code> has been <code>dispose</code>d with <code>!!clean === false</code>, no operation will be performed.
514 </li>
515</ul>
516Parameters:
517<ul>
518 <li><code>func</code> &mdash; Function to execute</li>
519</ul>
520Returns:
521<ul>
522 <li>Input <code>func</code></li>
523</ul>
524
525<h4 id="dispose"><code>function dispose(func, clean)</code></h4>
526Description:
527<ul>
528 <li>
529 "Disposes" a function that was run with <code>computed</code>, deregistering it so that it will no longer be called whenever any of its accessed reactive properties update.
530 The <code>clean</code> parameter controls whether calling <code>computed</code> with <code>func</code> will work or no-op.
531 </li>
532</ul>
533Parameters:
534<ul>
535 <li><code>func</code> &mdash; Function to dispose, omit to dispose the currently executing computed function</li>
536 <li><code>clean</code> &mdash; If truthy, only deregister the function from all dependencies, but allow it to be used with <code>computed</code> again in the future</li>
537</ul>
538Returns:
539<ul>
540 <li>Input <code>func</code> if <code>func</code> is valid, otherwise <code>undefined</code></li>
541</ul>
542
543## Authors
544Made with ❤ by Lua MacDougall ([foxgirl.dev](https://foxgirl.dev/))
545
546## License
547This project is licensed under [MIT](LICENSE).
548More info in the [LICENSE](LICENSE) file.
549
550<i>"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."</i> - [tl;drLegal](https://tldrlegal.com/license/mit-license)