UNPKG

17.8 kBMarkdownView Raw
1<p align="center">
2 <a href="https://www.npmjs.com/package/luar">
3 <img src="https://img.shields.io/npm/v/luar?style=flat-square">
4 </a>
5 <a href="https://www.npmjs.com/package/luar?activeTab=dependencies">
6 <img src="https://img.shields.io/badge/dependencies-none-blue?style=flat-square">
7 </a>
8 <a href="https://bundlephobia.com/result?p=luar">
9 <img src="https://img.shields.io/bundlephobia/minzip/luar?style=flat-square">
10 </a>
11 <a href="https://github.com/luawtf/Luar/actions">
12 <img src="https://img.shields.io/github/workflow/status/luawtf/Luar/continuous%20integration/master?style=flat-square">
13 </a>
14 <a href="https://coveralls.io/github/luawtf/Luar">
15 <img src="https://img.shields.io/coveralls/github/luawtf/Luar?style=flat-square">
16 </a>
17 <a href="https://github.com/luawtf/Luar/blob/master/LICENSE">
18 <img src="https://img.shields.io/github/license/luawtf/Luar?style=flat-square">
19 </a>
20</p>
21
22<h2 align="center">Luar &#x1F501;</h2>
23<p align="center"><i>
24 Luar provides functions for facilitating simple <a href="https://wikipedia.org/wiki/Reactive_programming">reactive programming</a> in JavaScript, inspired by Vue.js and Hyperactiv.
25 Luar is compatible with ECMAScript 5 (2009) and up and works without issue on Internet Explorer 9 and newer.
26</i></p>
27
28## Description
29Luar is a simple and concise implementation of the reactive programming paradigm for JavaScript (with included TypeScript support).
30Luar is also compatible with almost any browser released since 2010, being designed with both backwards and forward compatibility in mind.
31Additionally, Luar is very small, weighing at around 1 kilobyte when minified and gzipped, it only contains about 82 semicolons worth of code!
32
33Luar provides functions for "observing" JavaScript objects, for creating functions that are "computed" (known as computed tasks) which operate on the data inside of those objects, and for removing "computed" functions so that they are no longer executed.
34When the data in an observed object updates, any computed tasks that depend on that data are re-run.
35I find this functionality most useful for MVC (Model-View-Controller) style declarative UI creation!
36
37On that note, let's use Luar to make a reactive webpage that says hello to a user:
38```html
39<h2 id="hello-message"></h2>
40<input type="text" oninput="model.first = value" placeholder="First">
41<input type="text" oninput="model.last = value" placeholder="Last">
42
43<script>
44 const elem = document.getElementById("hello-message");
45
46 // Our model object contains first and last fields
47 const model = Luar.observe({ first: "", last: "" });
48 // "Declare" that the element's inner text should be something like Hello, first last!
49 Luar.computed(() => elem.innerText = `Hello, ${model.first} ${model.last}!`);
50</script>
51```
52Now the `hello-message` header will say hello to the user, updating its content as soon as the model changes (on input)!
53This is known as "declarative UI", where you declare the content of your UI and how it connects to your data, and the UI updates \~reactively\~.
54You can [view this example on JSFiddle](https://jsfiddle.net/luawtf/ghbtvexo/latest) (you may have to press Run before the example starts).
55
56Finally, here are the functions provided by Luar, with JSDoc and TypeScript annotations:
57```typescript
58/**
59 * Makes a JavaScript object reactive
60 * @param {Object} obj Object to observe
61 * @returns {Object} Original input `obj`, now with reactivity
62 */
63export declare function observe<T extends object>(obj: T): T;
64
65/**
66 * Executes a function as a computed task and record its dependencies. The task
67 * will then be re-run whenever its dependencies change
68 * @param {Function} task Function to run and register as a computed task
69 * @return {Function} Original input `task`, now registered as a computed task
70 */
71export declare function computed<T extends () => void>(task: T): T;
72
73/**
74 * Marks a function as "disposed" which will prevent it from being run as a
75 * computed task and remove it from the dependencies of reactive objects
76 * @param {Function} [task] Computed task function to dispose of, omit this
77 * parameter to dispose of the current computed task
78 */
79export declare function dispose(task?: (() => void) | null): void;
80```
81
82## Installation
83Luar is available on [NPM](https://www.npmjs.com/package/luar):
84```sh
85npm install luar
86```
87```javascript
88// Normal CommonJS modules:
89const { observe, computed, dispose } = require("luar");
90
91// Or for environments with backwards-compatible ES modules:
92import { observe, computed, dispose } from "luar";
93```
94
95Or, for people working without a bundler, it can be included from [UNPKG](https://www.unpkg.com/browse/luar@latest/):
96```html
97<script src="https://unpkg.com/luar"></script>
98<script>
99 Luar.observe({});
100 Luar.computed(function () {});
101 Luar.dispose(function () {});
102</script>
103```
104
105By default, the CommonJS import is unminified and the UNPKG import is minified using a Terser configuration designed for execution speed.
106These files are available in `luar/index.js` and `luar/index.min.js` respectively.
107
108## Usage
109 - [1. Basic reactivity](#1-basic-reactivity)
110 - [2. Multiple objects and computed properties](#2-multiple-objects-and-computed-properties)
111 - [3. Deep reactivity and implicit observation](#3-deep-reactivity-and-implicit-observation)
112 - [4. Cleaning up](#4-cleaning-up)
113 - [5. Reactivity pitfalls](#5-reactivity-pitfalls)
114
115### 1. Basic reactivity
116JavaScript is an imperative programming language, so if we evaluate the expression `z = x + y` and then change `y` or `x` to different numbers, `z`'s value does not update and remains out-of-date until we evaluate another expression that updates `z`.
117An example, in normal JavaScript:
118```javascript
119let coords = { x: 10, y: 20 };
120let z = coords.x + coords.y;
121
122console.log(z); // Output: "30" ✔
123
124coords.x += 10;
125coords.y = 21;
126
127console.log(z); // Output: "30" ✘
128```
129
130In a reactive environment, the expression `z = x + y` is a "declaration" that `z` will be the sum of `x` and `y`.
131The "declaration" part means that if `y` or `x` changes, so does `z`!
132This example uses Luar's observe and computed functions to "declare" that `z = x + y` using a computed task.
133```javascript
134let coords = observe({ x: 10, y: 20 });
135let z; computed(() => z = coords.x + coords.y);
136
137console.log(z); // Output: "30" ✔
138
139coords.x += 10;
140coords.y = 21;
141
142console.log(z); // Output: "41" ✔
143```
144As you can see, this code uses a computed task that sets `z` to the result of `coords.x + coords.y`.
145The task will be re-run whenever `coords.x` or `coords.y` changes, meaning that `z` will stay up-to-date with the values in the `coords` object!
146
147### 2. Multiple objects and computed properties
148In this example we create a computed task that depends on multiple properties and sub-properties of multiple objects.
149Notice the use of a sub-object which is implicitly reactive and even swapping out sub-objects with new ones.
150```javascript
151// Setting up some reactive objects that contain some data about a specific US president ...
152const person = observe({
153 name: { first: "George", last: "Washington" },
154 age: 288
155});
156const account = observe({
157 user: "big-george12",
158 password: "IHateTheQueen!1"
159});
160
161// Declare that we will output a log message whenever person.name.first, account.user, or person.age are updated
162computed(() => console.log(
163 `${person.name.first}'s username is ${account.user} (${person.age} years old)`
164)); // Output "George's username is big-george12 (288 years old)"
165
166// Changing reactive properties will only run computed tasks that depend on them
167account.password = "not-telling"; // Does not output (no computed task depends on this)
168
169// All operators work when updating properties
170account.user += "3"; // Output "George's username is big-george123 (288 years old)"
171person.age++; // Output "George's username is big-george123 (289 years old)"
172
173// You can even replace objects (and arrays) entirely!
174// This will automatically observe this new object and will correctly carry across any dependent computed tasks
175person.name = {
176 first: "Abraham",
177 last: "Lincoln"
178}; // Output "Abraham's username is big-george123 (289 years old)"
179```
180
181You can also link up multiple computed tasks which will be run as one "task".
182Computed tasks will trigger other computed tasks if they change values that have dependencies, even able to trigger multiple other tasks at once!
183The only restriction is that a computed task cannot trigger itself, as that would always result in an infinite loop.
184```javascript
185// Create our nums object, with some default values for properties that will be computed
186const nums = observe({
187 a: 33, b: 23, c: 84,
188 x: 0,
189 sumAB: 0, sumAX: 0, sumCX: 0,
190 sumAllSums: 0
191});
192
193// Declare that (x) will be equal to (a + b + c)
194computed(() => nums.x = nums.a + nums.b + nums.c);
195// Declare that (sumAB) will be equal to (a + b)
196computed(() => nums.sumAB = nums.a + nums.b);
197// Declare that (sumAX) will be equal to (a + x)
198computed(() => nums.sumAX = nums.a + nums.x);
199// Declare that (sumCX) will be equal to (c + x)
200computed(() => nums.sumCX = nums.c + nums.x);
201// Declare that (sumAllSums) will be equal to (sumAB + sumAX + sumCX)
202computed(() => nums.sumAllSums = nums.sumAB + nums.sumAX + nums.sumCX);
203
204// Now lets check the (sumAllSums) value
205console.log(nums.sumAllSums); // Output "453"
206
207// Notice that when we update one value ...
208nums.c += 2;
209// ... all the other values update! (since we declared them as such)
210console.log(nums.sumAllSums); // Output "459"
211```
212
213### 3. Deep reactivity and implicit observation
214Computed tasks can reference properties at any arbitrary depth in a reactive object and will update if any of the objects in the chain are changed.
215Everything works as expected, even circular references.
216Note the use of `toString()`, when executed it will automatically add `eventSummary.description.full` as another dependency of the computed task.
217```javascript
218const eventSummary = observe({
219 title: "Important Meeting #283954",
220 description: "Will be meeting with the president of BigImportantFirmCo to talk business",
221 summary: null,
222 guestInfo: {
223 you: { name: "", id: 7999782267 },
224 president: { name: "Mr. Firm", id: 4160971388 }
225 }
226});
227
228computed(() =>
229 console.log("" + eventSummary.description)
230); // Output "Will be meeting with the president of BigImportantFirmCo to talk business"
231
232eventSummary.description = {
233 short: "Will be meeting with the president of ...",
234 full: "Will be meeting with the president of BigImportantFirmCo to talk business.\nMake sure to arrive by 11:30!",
235 toString() {
236 return this.full;
237 }
238}; // Output "Will be meeting with the president of BigImportantFirmCo to talk business.\nMake sure to arrive by 11:30!"
239
240// Circular reactive references!
241eventSummary.summary = eventSummary;
242
243eventSummary.summary.summary.summary.summary.summary.description.full +=
244 "\n(remember to show off how cool reactivity is)";
245// Output ... business.\nMake sure to arrive by 11:30!\n(remember to show off how cool reactivity is)"
246```
247
248Another example of implicit observation of objects.
249Any non-reactive objects set onto reactive objects will be "infected" and become reactive-capable as well, even if they are removed from the reactive object later.
250Protip: you can check if an object is reactive by checking for the existence of the `__luar` property (`if (obj.__luar) {}`).
251```javascript
252// Create some data, note that this data is not reactive!
253const someData = {
254 m: 33,
255 x: null,
256 y: { a: true, b: false }
257};
258
259// And make some more data that *is* reactive
260const model = observe({
261 title: "Crucial Information",
262 color: "red",
263 data: null
264});
265
266// But oh no! We added the non-reactive data into a reactive object!
267// This makes it implicitly reactive, now all of someData's properties and sub-objects are all reactive
268model.data = someData;
269
270// Let's use that reactivity and listen on someData.m
271computed(() => console.log(someData.m)); // Output "33"
272
273// Look, reactive!!
274model.data.m++; // Output "34"
275someData.m++; // Output "35"
276```
277
278Since functions are also objects, functions can be observed and have reactive properties.
279However, functions are currently exempt from implicit observation (both recursive and being set onto a reactive property) and can only be made reactive by passing them directly to `observe(obj)`.
280```javascript
281function fn1() {}; fn1.x = 10;
282function fn2() {}; fn2.y = 20;
283function fn3() {}; fn3.z = 30;
284
285observe(fn1);
286fn1.x // <-- Will be reactive (observed directly)
287
288observe({ subFunc: fn2 });
289fn2.y // <-- Will NOT be reactive (recursive observation)
290
291const obj = observe({ setFunc: null });
292obj.setFunc = fn3;
293fn3.z // <-- Will NOT be reactive (set onto reactive property)
294```
295
296### 4. Cleaning up
297Luar provides the `dispose(fn)` function for destroying computed tasks, but there is currently no way to de-reactify an object.
298Observing an object is a non-reversible operation, but you could create a clone of the reactive object (which would not be reactive) like `nonReactiveObj = { ...reactiveObj }`.
299
300Without the usage of `dispose(fn)`, Luar reactive objects and computed tasks still get garbage collected when they go entirely out of scope:
301```javascript
302// To demonstrate how computed tasks and reactive objects are garbage collected, lets create some!
303
304{
305 const thing = observe({ hi: "Hello, world" });
306 // `thing` now exists
307
308 {
309 computed(() => console.log(thing.hi)); // Output "Hello, world"
310 // `thing.hi` now has a computed task depending on it
311
312 thing.hi += "!"; // Output "Hello, world!"
313 }
314 // `thing` and `thing.hi`'s computed task still exist
315
316 thing.hi += "!"; // Output "Hello, world!!"
317}
318// `thing` has gone out of scope!
319// Both `thing` and `thing.hi`'s computed task get garbage collected.
320```
321
322Now let's incorporate some early computed task removal!
323```javascript
324// Create the thing
325const thing = observe({ hi: "Hello, world" });
326
327// Attach our shiny new task
328const task = computed(() => console.log(thing.hi)); // Output "Hello, world"
329
330thing.hi += "!"; // Output "Hello, world!"
331
332// Now dispose of the task!
333dispose(task);
334
335thing.hi += "!"; // No output, since the computed task is gone (and garbage collected)
336```
337
338### 5. Reactivity pitfalls
339Luar isn't perfect and hacking reactivity into a language like JavaScript is not very elegant.
340This section presents a comprehensive list of cases where errors are generated or properties aren't reactive.
341
342*Computed tasks can trigger each other in infinite loops:*
343```javascript
344const obj = { x: 10, y: 20 };
345observe(obj);
346
347// This task depends on x and updates y if x > 20
348computed(() => {
349 if (obj.x > 20) obj.y++;
350});
351// This task depends on y and updates x if y > 20
352computed(() => {
353 if (obj.y > 20) obj.x++;
354});
355
356// Since both tasks depend on each other, bad things can happen
357obj.x += 11; // Uncaught Error: [Luar] ERR Maximum computed task length exceeded (stack overflow!)
358```
359
360*Unlike other reactivity libraries which mangle arrays, Luar does not hack reactivity into arrays:*
361```javascript
362const obj = { arr: [1, 2, 3] };
363observe(obj);
364
365computed(() => console.log(obj.arr)); // Output "1,2,3"
366
367obj.arr[2] = 4; // No output, arrays are not reactive!
368obj.arr.push(5); // Still no output, as this library does not replace array methods
369
370// If you want to use arrays, do it like this:
371// 1. Run your operations
372obj.arr[2] = 3;
373obj.arr[3] = 4;
374obj.arr.push(5);
375// 2. Then set the array to itself
376obj.arr = obj.arr; // Output "1,2,3,4,5"
377```
378
379*Properties added after observation are not reactive:*
380```javascript
381const obj = { y: 20 };
382observe(obj);
383
384obj.x = 10;
385
386computed(() => console.log(obj.x)); // Output "10"
387computed(() => console.log(obj.y)); // Output "20"
388
389obj.y += 2; // Output "22"
390
391obj.x += 2; // No output, as this property was added after observation
392
393observe(obj);
394
395obj.x += 2; // Still no output, as objects cannot be re-observed
396```
397
398*Properties on a reactive object's prototype are not reactive:*
399```javascript
400const objPrototype = {
401 x: 10
402};
403const obj = {
404 y: 20
405};
406Object.setPrototypeOf(obj, objPrototype);
407
408observe(obj);
409
410computed(() => console.log(obj.x)); // Output "10"
411computed(() => console.log(obj.y)); // Output "20"
412
413obj.y = 21; // Output "21"
414
415obj.x = 11; // No output, as this isn't an actual property of `obj`
416objPrototype.x = 12; // No output, as prototypes are not reactive
417```
418
419*Properties defined as non-enumerable or non-configurable cannot be made reactive:*
420```javascript
421const obj = {
422 z: 30
423};
424Object.defineProperty(obj, "x", {
425 enumerable: false,
426 value: 10
427});
428Object.defineProperty(obj, "y", {
429 configurable: false,
430 value: 20
431});
432
433observe(obj);
434
435computed(() => console.log(obj.x)); // Output "10"
436computed(() => console.log(obj.y)); // Output "20"
437computed(() => console.log(obj.z)); // Output "30"
438
439obj.z++; // Output "31"
440
441obj.x++; // No output as this property is non-enumerable
442obj.y++; // No output as this property is non-configurable
443```
444
445*The "\_\_proto\_\_" property will never be made reactive:*
446```javascript
447const obj = {};
448Object.defineProperty(obj, "__proto__", { value: 10 });
449
450observe(obj);
451
452computed(() => console.log(obj.__proto__)); // Output "10"
453
454obj.__proto__++; // No output as properties named __proto__ are ignored
455```
456
457## Authors
458Made with ❤ by Lua MacDougall ([lua.wtf](https://lua.wtf/))
459
460## License
461This project is licensed under [MIT](LICENSE).
462More info in the [LICENSE](LICENSE) file.
463
464<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)