├── .gitignore ├── examples ├── pony-vid.gif ├── counter-vid.gif ├── debounce-vid.gif ├── concatenator-vid.gif ├── counter.html ├── concatenator.html ├── debounce.html └── pony.html ├── .npmignore ├── .editorconfig ├── lib ├── ignore.js ├── patella.js ├── dispose.js ├── patella.d.ts ├── computed.js ├── reactive.js └── util.js ├── LICENSE ├── .github └── workflows │ └── actions.yml ├── package.json ├── rollup.config.js ├── README.md └── test └── patella.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | -------------------------------------------------------------------------------- /examples/pony-vid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luavixen/Patella/HEAD/examples/pony-vid.gif -------------------------------------------------------------------------------- /examples/counter-vid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luavixen/Patella/HEAD/examples/counter-vid.gif -------------------------------------------------------------------------------- /examples/debounce-vid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luavixen/Patella/HEAD/examples/debounce-vid.gif -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .editorconfig 3 | .gitignore 4 | test 5 | coverage 6 | examples 7 | rollup.config.js 8 | -------------------------------------------------------------------------------- /examples/concatenator-vid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luavixen/Patella/HEAD/examples/concatenator-vid.gif -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | charset = utf-8 4 | end_of_line = lf 5 | 6 | indent_size = 2 7 | indent_style = space 8 | 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /lib/ignore.js: -------------------------------------------------------------------------------- 1 | import { 2 | isObject, isFunction, 3 | hasOwnProperty, 4 | HINT_OBSERVE, defineHint, 5 | MESSAGE_NOT_OBJECT, throwError 6 | } from "./util.js"; 7 | 8 | /** See lib/patella.d.ts */ 9 | export function ignore(object) { 10 | if (!isObject(object) && !isFunction(object)) { 11 | throwError(MESSAGE_NOT_OBJECT); 12 | } 13 | 14 | if (!hasOwnProperty(object, HINT_OBSERVE)) { 15 | defineHint(object, HINT_OBSERVE); 16 | } 17 | 18 | return object; 19 | } 20 | -------------------------------------------------------------------------------- /examples/counter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Patella Example - Counter 8 | 9 | 10 | 11 |
12 |

Click Counter

13 | 14 |
15 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Lua MacDougall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration and deployment 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | release: 11 | types: 12 | - published 13 | 14 | jobs: 15 | test: 16 | name: Run test suite and upload coverage 17 | runs-on: ubuntu-20.04 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-node@v2 21 | with: 22 | node-version: 16 23 | - name: Install dependencies 24 | run: npm ci 25 | - name: Run tests 26 | run: npm test 27 | - name: Upload coverage 28 | uses: coverallsapp/github-action@v1.1.2 29 | with: 30 | github-token: ${{ secrets.GITHUB_TOKEN }} 31 | npm: 32 | name: Publish to npm 33 | if: github.event_name == 'release' 34 | needs: test 35 | runs-on: ubuntu-20.04 36 | steps: 37 | - uses: actions/checkout@v2 38 | - uses: actions/setup-node@v2 39 | with: 40 | node-version: 16 41 | registry-url: https://registry.npmjs.org 42 | - name: Install dependencies 43 | run: npm ci 44 | - name: Publish 45 | run: npm publish 46 | env: 47 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 48 | -------------------------------------------------------------------------------- /examples/concatenator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Patella Example - Concatenator 8 | 9 | 10 | 11 |
12 |

Concatenator

13 | 14 | 15 |

16 |
17 | 31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /examples/debounce.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Patella Example - Debounced Search 8 | 9 | 10 | 11 |
12 |

Debounced Search

13 | 14 | 15 |
16 | 37 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /lib/patella.js: -------------------------------------------------------------------------------- 1 | /* 2 | * == Patella == 3 | * Extremely small, fast and compatible reactive programming library. 4 | * 5 | * Version 2.2.2 6 | * 7 | * MIT License 8 | * 9 | * Copyright (c) 2021 Lua MacDougall 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a copy 12 | * of this software and associated documentation files (the "Software"), to deal 13 | * in the Software without restriction, including without limitation the rights 14 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | * copies of the Software, and to permit persons to whom the Software is 16 | * furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included in 19 | * all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | * SOFTWARE. 28 | */ 29 | 30 | export { observe } from "./reactive.js"; 31 | export { ignore } from "./ignore.js"; 32 | export { computed } from "./computed.js"; 33 | export { dispose } from "./dispose.js"; 34 | -------------------------------------------------------------------------------- /lib/dispose.js: -------------------------------------------------------------------------------- 1 | import { computedLock, computedQueue, computedI } from "./computed.js"; 2 | import { 3 | isFunction, 4 | hasOwnProperty, 5 | HINT_DISPOSE, HINT_DEPENDS, defineHint, 6 | MESSAGE_NOT_FUNCTION, throwError 7 | } from "./util.js"; 8 | 9 | /** See lib/patella.d.ts */ 10 | export function dispose(func, clean) { 11 | if (func == null) { 12 | func = computedQueue[computedI]; 13 | if (!func) { 14 | throwError("Tried to dispose of current computed function while not running a computed function", true); 15 | } 16 | } else if (!isFunction(func)) { 17 | throwError(MESSAGE_NOT_FUNCTION); 18 | } 19 | 20 | // Only execute if the function has not been disposed yet 21 | if (!hasOwnProperty(func, HINT_DISPOSE)) { 22 | // Only define disposed property if we aren't cleaning 23 | if (!clean) defineHint(func, HINT_DISPOSE); 24 | 25 | // Remove from dependant reactive objects 26 | var depends = func[HINT_DEPENDS]; 27 | if (depends) { 28 | defineHint(func, HINT_DEPENDS, clean ? [] : void 0); 29 | for (var i = 0; i < depends.length; i++) { 30 | depends[i](func); 31 | } 32 | } 33 | 34 | // Remove from the queue if locked and pending execution 35 | if (computedLock) { // Not required, but saves a `lastIndexOf` call on an empty array for like 6 bytes 36 | var i = computedQueue.lastIndexOf(func); 37 | if (i > computedI) computedQueue.splice(i, 1); 38 | } 39 | } 40 | 41 | // Only return the function if it was specified as an argument 42 | if (!computedLock) return func; 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patella", 3 | "version": "2.2.2", 4 | "description": "Patella is a library for reactive programming in JavaScript, inspired by Hyperactiv and Vue.js.", 5 | "author": "Lua MacDougall (https://foxgirl.dev/)", 6 | "license": "MIT", 7 | "keywords": [ 8 | "reactive", 9 | "react", 10 | "computed", 11 | "computed properties", 12 | "properties", 13 | "observable", 14 | "model", 15 | "mvc", 16 | "mvvc", 17 | "vue", 18 | "vue.js", 19 | "hyperactiv" 20 | ], 21 | "type": "module", 22 | "main": "./dist/patella.cjs.js", 23 | "module": "./lib/patella.js", 24 | "types": "./lib/patella.d.ts", 25 | "jsdelivr": "./dist/patella.iife.min.js", 26 | "unpkg": "./dist/patella.iife.min.js", 27 | "exports": { 28 | ".": { 29 | "require": "./dist/patella.cjs.js", 30 | "default": "./lib/patella.js" 31 | } 32 | }, 33 | "scripts": { 34 | "test": "c8 --reporter lcov --reporter text-summary mocha --ui tdd ./test/patella.test.js", 35 | "build": "rollup --config rollup.config.js", 36 | "prepublishOnly": "npm test && npm run build" 37 | }, 38 | "devDependencies": { 39 | "c8": "^7.8.0", 40 | "chai": "^4.3.4", 41 | "mocha": "^9.0.3", 42 | "rollup": "^2.56.2", 43 | "terser": "^5.7.1", 44 | "uglify-js": "^3.14.1" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "git+https://github.com/luavixen/Patella.git" 49 | }, 50 | "bugs": { 51 | "url": "https://github.com/luavixen/Patella/issues" 52 | }, 53 | "homepage": "https://github.com/luavixen/Patella#readme" 54 | } 55 | -------------------------------------------------------------------------------- /lib/patella.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Makes an object and its properties reactive recursively. 3 | * Subobjects (but not subfunctions!) will also be observed. 4 | * Note that `observe` does not create a new object, it mutates the object passed into it: `observe(object) === object`. 5 | * @param {Object} object Object or function to make reactive 6 | * @returns {Object} Input `object`, now reactive 7 | */ 8 | export declare function observe(object: T): T; 9 | 10 | /** 11 | * Prevents an object from being made reactive, `observe` will do nothing. 12 | * Note that `ignore` is not recursive, so subobjects can still be made reactive by calling `observe` on them directly. 13 | * @param {Object} object Object or function to ignore 14 | * @returns {Object} Input `object`, now permanently ignored 15 | */ 16 | export declare function ignore(object: T): T; 17 | 18 | /** 19 | * Calls `func` with no arguments and records a list of all the reactive properties it accesses. 20 | * `func` will then be called again whenever any of the accessed properties are mutated. 21 | * Note that if `func` has been `dispose`d with `!!clean === false`, no operation will be performed. 22 | * @param {Function} func Function to execute 23 | * @returns {Function} Input `func` 24 | */ 25 | export declare function computed void>(func: T): T; 26 | 27 | /** 28 | * "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. 29 | * The clean parameter controls whether calling `computed` with `func` will work or no-op. 30 | * @param {Function} [func] Function to dispose, omit to dispose the currently executing computed function 31 | * @param {boolean} [clean] If truthy, only deregister the function from all dependencies, but allow it to be used with `computed` again in the future 32 | * @returns {Function} Input `func` if `func` is valid, otherwise `undefined` 33 | */ 34 | export declare function dispose(func?: null, clean?: boolean | null): void; 35 | export declare function dispose void>(func: T, clean?: boolean | null): T; 36 | -------------------------------------------------------------------------------- /lib/computed.js: -------------------------------------------------------------------------------- 1 | import { 2 | isFunction, 3 | hasOwnProperty, 4 | HINT_DISPOSE, HINT_DEPENDS, defineHint, 5 | MESSAGE_NOT_FUNCTION, throwError 6 | } from "./util.js"; 7 | 8 | /** Maximum queue length */ 9 | var MAX_QUEUE = 2000; 10 | 11 | /** Is the queue being executed? */ 12 | export var computedLock = false; 13 | /** Queue of computed functions to be called */ 14 | export var computedQueue = []; 15 | /** Current index into `computedQueue` */ 16 | export var computedI = 0; 17 | 18 | /** 19 | * Throws an error indicating that the computed queue has overflowed 20 | */ 21 | function computedOverflow() { 22 | var message = "Computed queue overflow! Last 10 functions in the queue:"; 23 | 24 | var length = computedQueue.length; 25 | for (var i = length - 11; i < length; i++) { 26 | var func = computedQueue[i]; 27 | message += 28 | "\n" 29 | + (i + 1) 30 | + ": " 31 | + (func.name || "anonymous"); 32 | } 33 | 34 | throwError(message, true); 35 | } 36 | 37 | /** 38 | * Attempts to add a function to the computed queue, then attempts to lock and execute the computed queue 39 | * @param func Function to queue 40 | */ 41 | export function computedNotify(func) { 42 | if (hasOwnProperty(func, HINT_DISPOSE)) return; 43 | 44 | // Only add to the queue if not already pending execution 45 | if (computedQueue.lastIndexOf(func) >= computedI) return; 46 | computedQueue.push(func); 47 | 48 | // Make sure that the function in question has a depends hint 49 | if (!hasOwnProperty(func, HINT_DEPENDS)) { 50 | defineHint(func, HINT_DEPENDS, []); 51 | } 52 | 53 | // Attempt to lock and execute the queue 54 | if (!computedLock) { 55 | computedLock = true; 56 | 57 | try { 58 | for (; computedI < computedQueue.length; computedI++) { 59 | // Indirectly call the function to avoid leaking `computedQueue` as `this` 60 | (0, computedQueue[computedI])(); 61 | if (computedI > MAX_QUEUE) /* @__NOINLINE */ computedOverflow(); 62 | } 63 | } finally { 64 | computedLock = false; 65 | computedQueue = []; 66 | computedI = 0; 67 | } 68 | } 69 | } 70 | 71 | /** See lib/patella.d.ts */ 72 | export function computed(func) { 73 | if (!isFunction(func)) { 74 | throwError(MESSAGE_NOT_FUNCTION); 75 | } 76 | 77 | computedNotify(func); 78 | return func; 79 | } 80 | -------------------------------------------------------------------------------- /lib/reactive.js: -------------------------------------------------------------------------------- 1 | import { computedQueue, computedI, computedNotify } from "./computed.js"; 2 | import { 3 | isObject, isFunction, 4 | hasOwnProperty, defineProperty, 5 | HINT_OBSERVE, HINT_DEPENDS, defineHint, 6 | MESSAGE_NOT_OBJECT, throwError 7 | } from "./util.js"; 8 | 9 | /** 10 | * Generates a property descriptor for a reactive property 11 | * @param value Initial property value 12 | * @returns Property descriptor object 13 | */ 14 | function reactiveProperty(value) { 15 | if (isObject(value)) reactiveObserve(value); 16 | 17 | // List of computed functions that depend on this property 18 | var depends = []; 19 | /** 20 | * Remove a computed function from this reactive property 21 | * @param func Computed function to remove 22 | */ 23 | function dependsRemove(func) { 24 | var i = depends.lastIndexOf(func); 25 | if (i >= 0) depends.splice(i, 1); 26 | } 27 | 28 | return { 29 | get: function() { 30 | // Add the current executing computed function to this reactive property's dependencies 31 | var func = computedQueue[computedI]; 32 | if (func) { 33 | var i = depends.lastIndexOf(func); 34 | if (i < 0) { 35 | // Add them to our dependencies 36 | depends.push(func); 37 | // Add us to their dependants 38 | func[HINT_DEPENDS].push(dependsRemove); 39 | } 40 | } 41 | 42 | return value; 43 | }, 44 | set: function(newValue) { 45 | if (isObject(newValue)) reactiveObserve(newValue); 46 | value = newValue; 47 | 48 | // Notify all dependencies 49 | for (var i = 0; i < depends.length; i++) { 50 | computedNotify(depends[i]); 51 | } 52 | } 53 | }; 54 | } 55 | 56 | /** 57 | * Observes an object by making all of its enumerable properties reactive 58 | * @param object Object to observe 59 | */ 60 | function reactiveObserve(object) { 61 | if (hasOwnProperty(object, HINT_OBSERVE)) return; 62 | defineHint(object, HINT_OBSERVE); 63 | 64 | for (var key in object) { 65 | if (hasOwnProperty(object, key)) { 66 | try { 67 | defineProperty(object, key, reactiveProperty(object[key])); 68 | } catch (err) {} 69 | } 70 | } 71 | } 72 | 73 | /** See lib/patella.d.ts */ 74 | export function observe(object) { 75 | if (!isObject(object) && !isFunction(object)) { 76 | throwError(MESSAGE_NOT_OBJECT); 77 | } 78 | 79 | reactiveObserve(object); 80 | return object; 81 | } 82 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { minify as minifyTerser } from "terser"; 2 | import { minify as minifyUglify } from "uglify-js"; 3 | 4 | import { mkdir, writeFile } from "fs/promises"; 5 | 6 | const uglify = { 7 | name: "uglify", 8 | config: { 9 | ie8: true, 10 | webkit: true, 11 | v8: true, 12 | compress: { 13 | passes: 4, 14 | hoist_funs: true, 15 | hoist_vars: true, 16 | hoist_props: true, 17 | inline: 2, 18 | reduce_funcs: false, 19 | typeofs: false, 20 | unsafe_comps: true 21 | }, 22 | output: { 23 | comments: true 24 | } 25 | }, 26 | renderChunk(code, chunk, options) { 27 | const result = minifyUglify(code, { 28 | ...uglify.config, 29 | sourceMap: !!options.sourcemap, 30 | toplevel: /^c(ommon)?js$/.test(options.format) 31 | }); 32 | return result.error ? Promise.reject(result.error) : Promise.resolve(result); 33 | } 34 | }; 35 | 36 | const terser = { 37 | name: "terser", 38 | config: { 39 | ecma: 5, 40 | ie8: true, 41 | safari10: true, 42 | compress: { 43 | passes: 4, 44 | collapse_vars: true, 45 | hoist_funs: true, 46 | hoist_vars: true, 47 | hoist_props: true, 48 | typeofs: false, 49 | unsafe_comps: true, 50 | pure_funcs: [ 51 | "hasOwnProperty", "createSymbol", "isObject", "isFunction" 52 | ] 53 | } 54 | }, 55 | renderChunk(code, chunk, options) { 56 | return minifyTerser(code, { 57 | ...terser.config, 58 | sourceMap: !!options.sourcemap, 59 | module: /^(esm?|module)$/.test(options.format), 60 | toplevel: /^c(ommon)?js$/.test(options.format) 61 | }); 62 | } 63 | }; 64 | 65 | const packageCJS = { 66 | name: "package-cjs", 67 | async buildEnd(error) { 68 | if (error) return; 69 | try { await mkdir("./dist"); } catch (error) {} 70 | await writeFile("./dist/package.json", `{ "type": "commonjs" }\n`); 71 | } 72 | }; 73 | 74 | const input = (input, plugins = [], ...output) => ({ input, output, plugins }); 75 | const output = (file, format, sourcemap = true, plugins = []) => ({ 76 | file, 77 | format, 78 | sourcemap, 79 | plugins, 80 | name: "Patella", 81 | indent: " ", 82 | esModule: false, 83 | strict: false, 84 | freeze: false 85 | }); 86 | 87 | export default input("lib/patella.js", [packageCJS], 88 | output("dist/patella.cjs.js", "cjs"), 89 | output("dist/patella.iife.min.js", "iife", "hidden", [uglify, terser]) 90 | ); 91 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | // Global object/function references 2 | var _Object = Object; 3 | var _Object_hasOwnProperty = _Object.hasOwnProperty; 4 | var _Array_isArray = Array.isArray; 5 | var _Error = Error; 6 | var _TypeError = TypeError; 7 | 8 | /** Reference to global Object.defineProperty */ 9 | export var defineProperty = _Object.defineProperty; 10 | 11 | /** 12 | * Checks if an object has a specified property as its own property (ignores prototype properties and `__proto__`) 13 | * @param object Object to check 14 | * @param key Property key 15 | * @returns Does `object` have the property `key`? 16 | */ 17 | export function hasOwnProperty(object, key) { 18 | return key !== "__proto__" && _Object_hasOwnProperty.call(object, key); 19 | } 20 | 21 | /** 22 | * Creates an ECMAScript 6 Symbol, falling back to a simple string in environments that do not support Symbols 23 | * @param description Symbol description 24 | * @returns Symbol object or string 25 | * @function 26 | */ 27 | /* c8 ignore start */ 28 | /* istanbul ignore next */ 29 | var createSymbol = 30 | typeof Symbol === "function" 31 | ? Symbol 32 | : function (description) { 33 | return "__" + description; 34 | }; 35 | /* c8 ignore stop */ 36 | 37 | /** Hint property to indicate if an object has been observed */ 38 | export var HINT_OBSERVE = createSymbol("observe"); 39 | /** Hint property to indicate if a function has been disposed */ 40 | export var HINT_DISPOSE = createSymbol("dispose"); 41 | /** Hint property that contains a function's dependency disposal callbacks */ 42 | export var HINT_DEPENDS = createSymbol("depends"); 43 | 44 | /** 45 | * Defines a hint property on an object 46 | * @param object Object to define property on 47 | * @param hint Property key 48 | * @param {*} [value] Property value, property will be made non-configurable if this is unset (`undefined`) 49 | */ 50 | export function defineHint(object, hint, value) { 51 | defineProperty(object, hint, { 52 | value: value, 53 | configurable: value !== void 0, 54 | enumerable: false, 55 | writable: false 56 | }); 57 | } 58 | 59 | /** 60 | * Checks if a value is a normal object, ignores functions and arrays 61 | * @param value Value to check 62 | * @returns Is `value` a normal object? 63 | */ 64 | export function isObject(value) { 65 | return value !== null && typeof value === "object" && !_Array_isArray(value); 66 | } 67 | 68 | /** 69 | * Checks if a value is a function 70 | * @param value Value to check 71 | * @return Is `value` a function? 72 | */ 73 | export function isFunction(value) { 74 | return typeof value === "function"; 75 | } 76 | 77 | /** Error message printed when an argument is of an incorrect type (not a normal object) */ 78 | export var MESSAGE_NOT_OBJECT = "Argument 'object' is not an object"; 79 | /** Error message printed when an argument is of an incorrect type (not a function) */ 80 | export var MESSAGE_NOT_FUNCTION = "Argument 'func' is not a function"; 81 | 82 | /** 83 | * Throws an error message 84 | * @param message Message to construct the error with 85 | * @param generic Should the more generic `Error` be thrown instead of `TypeError`? 86 | */ 87 | export function throwError(message, generic) { 88 | throw new (generic ? _Error : _TypeError)(message); 89 | } 90 | -------------------------------------------------------------------------------- /examples/pony.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Patella Example - Pony Browser 8 | 9 | 10 | 11 |
12 |

Pony Browser

13 | 14 |
    15 | 16 |
    17 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |

    24 | 25 | # Patella 🔁 26 | 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/). 27 | Patella is compatible with Chrome 5, Firefox 4, and Internet Explorer 9. 28 | 29 | The [patellar tendon is responsible for the well known "knee-jerk reaction"](https://wikipedia.org/wiki/Patellar_reflex). 30 | 31 | Jump 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 41 | Patella is available via [npm](https://www.npmjs.com/package/patella): 42 | ```sh 43 | $ npm install patella 44 | ``` 45 | ```javascript 46 | // ECMAScript module environments 47 | import { observe, ignore, computed, dispose } from "patella"; 48 | // CommonJS environments 49 | const { observe, ignore, computed, dispose } = require("patella"); 50 | ``` 51 | 52 | Or, for people working without a bundler, it can be included from [UNPKG](https://www.unpkg.com/browse/patella@latest/): 53 | ```html 54 | 55 | 61 | ``` 62 | 63 | Various other Patella builds are available in the [dist](./dist) folder, including sourcemaps and minified versions. 64 | 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). 65 | 66 | ## Usage 67 | Patella provides functions for observing object mutations and acting on those mutations automatically. 68 | 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)`: 69 | ```html 70 |

    Click Counter

    71 | 72 | 83 | ``` 84 | ![](./examples/counter-vid.gif)
    85 | View the [full source](./examples/counter.html) or [try it on JSFiddle](https://jsfiddle.net/luawtf/hL6g4emk/latest). 86 | 87 | Notice how in the above example, the `