├── .eslintignore ├── .eslintrc ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── esbuild.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── benchmark.ts ├── defaults.ts ├── example.md ├── index.test.ts ├── index.ts ├── lib.ts └── models.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | demo 4 | esbuild.js 5 | coverage 6 | jest.config.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "rules": { 11 | "no-console": 2, 12 | "no-debugger": 2, 13 | "no-var": 2, 14 | "no-unused-vars": 0, 15 | "@typescript-eslint/no-explicit-any": 0, 16 | "@typescript-eslint/ban-types": 0 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log* 2 | lerna-debug.log* 3 | 4 | node_modules 5 | dist 6 | 7 | .vscode/* 8 | !.vscode/extensions.json 9 | .idea 10 | .DS_Store 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | *.sw? 16 | 17 | coverage -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | demo 4 | coverage -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | demo -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #### 2.1.2 (2023-01-17) 2 | 3 | ##### Chores 4 | 5 | * **deps:** bump json5 from 2.2.1 to 2.2.3 ([7081adca](https://github.com/JointlyTech/object-loudifier/commit/7081adca5dd53f087b0153bcbf2613d47ceada3f)) 6 | 7 | #### 2.1.1 (2023-01-17) 8 | 9 | ##### Chores 10 | 11 | * linting ([09e54b63](https://github.com/JointlyTech/object-loudifier/commit/09e54b63d291c6eab9e8780d31186d19846e487e)) 12 | 13 | ##### Documentation Changes 14 | 15 | * added usage example ([44167021](https://github.com/JointlyTech/object-loudifier/commit/44167021b46fa4d5320cb3443655249ef55d192c)) 16 | 17 | ##### Performance Improvements 18 | 19 | * moved listeners from obj to map. +10% ([a97200d8](https://github.com/JointlyTech/object-loudifier/commit/a97200d8e2f38ad796ebbf2b63397ba2ac20e047)) 20 | * small perf improvements ([6234cf97](https://github.com/JointlyTech/object-loudifier/commit/6234cf97d4b29cedf93a0301203c3382dda739a1)) 21 | 22 | ##### Refactors 23 | 24 | * internal naming and folders refactoring ([0ef8fa83](https://github.com/JointlyTech/object-loudifier/commit/0ef8fa8353e07798b8fb795036c13d2708105f1c)) 25 | 26 | ### 2.1.0 (2022-12-21) 27 | 28 | ##### Chores 29 | 30 | * added shortcut for coverage command and removed redundant conf from jest config ([04aeacc1](https://github.com/JointlyTech/object-loudifier/commit/04aeacc1dc68a06d7fc04a4a47de947807649323)) 31 | * lint ([59fc14c5](https://github.com/JointlyTech/object-loudifier/commit/59fc14c5d39767a5e6c96c97de056d4b4bfb2765)) 32 | * better type enforcing ([5a88f919](https://github.com/JointlyTech/object-loudifier/commit/5a88f9191cc8e5ecbdf09b65663ad2f8aa1f4f25)) 33 | 34 | ##### Documentation Changes 35 | 36 | * added benchmark n. 6 ([aad26879](https://github.com/JointlyTech/object-loudifier/commit/aad268791da57ab6239277a7307a73341a9b9a2d)) 37 | 38 | ##### Bug Fixes 39 | 40 | * emit now return correct propName even if used with wildcards ([c37951c8](https://github.com/JointlyTech/object-loudifier/commit/c37951c897c00d1a217013b66c5f19e424af05e0)) 41 | 42 | ##### Other Changes 43 | 44 | * //github.com/JointlyTech/object-loudifier ([a4edda6b](https://github.com/JointlyTech/object-loudifier/commit/a4edda6b808a15d70c3581ea35012759f014f427)) 45 | 46 | ##### Refactors 47 | 48 | * changed name for onOptions for better readability ([725a5416](https://github.com/JointlyTech/object-loudifier/commit/725a5416bc0621c6755c442bcf75878591e5a14e)) 49 | 50 | ##### Tests 51 | 52 | * fixed some test to use latest features + adding test to improve code coverage ([4e7f3370](https://github.com/JointlyTech/object-loudifier/commit/4e7f33709cdae55e9e864d8699712f4ed7c0e73d)) 53 | * added wildcard prop test ([d2cd44d8](https://github.com/JointlyTech/object-loudifier/commit/d2cd44d84dcc5c7fc661d64add593fd82bc0e96a)) 54 | 55 | ## 2.0.0 (2022-12-15) 56 | 57 | ##### Chores 58 | 59 | * fixed benchmark execution ([348edbea](https://github.com/JointlyTech/object-loudifier/commit/348edbea31f59e074ac5d7aa9e3f078f5c3efe1a)) 60 | * enforced type checking + exporting loud object type ([b61f21ad](https://github.com/JointlyTech/object-loudifier/commit/b61f21ad22dbac4e89ddf75828668d1be0c3ff66)) 61 | 62 | ##### Documentation Changes 63 | 64 | * improved benchmark explanation ([dd1d2fac](https://github.com/JointlyTech/object-loudifier/commit/dd1d2fac61bd7ceb1ccb7bc85c2b1eec2483f3c9)) 65 | * improved bubbling priority explanation ([8c29488e](https://github.com/JointlyTech/object-loudifier/commit/8c29488e16378249439ac722b840cbdb8678eb5f)) 66 | 67 | ##### New Features 68 | 69 | * added emittedmetadata + internal checks refactor + tests ([fb5d8277](https://github.com/JointlyTech/object-loudifier/commit/fb5d8277428bee6c328a109040de32ac7e1092a4)) 70 | 71 | ##### Other Changes 72 | 73 | * allow nesting mechanism + internal refactor + loudify options parameter + tests ([7e7e1c35](https://github.com/JointlyTech/object-loudifier/commit/7e7e1c35a83418000b887f065b2bd1f69b26345f)) 74 | 75 | ##### Refactors 76 | 77 | * internal naming changes ([41589031](https://github.com/JointlyTech/object-loudifier/commit/41589031af41d8faf8b5482924ba9ed1ee85841e)) 78 | 79 | ### 1.1.0 (2022-12-14) 80 | 81 | ##### Chores 82 | 83 | * added benchmarks ([ee157fe0](https://github.com/JointlyTech/object-loudifier/commit/ee157fe07bdcfb15ff34005fd1c8f9d467c3b4be)) 84 | * added things to todo list ([923d6d58](https://github.com/JointlyTech/object-loudifier/commit/923d6d587f9df934ecedc9abcd237acc3f324f1d)) 85 | * typo fix ([86452201](https://github.com/JointlyTech/object-loudifier/commit/86452201d51e3ce31e595223a97be0f63e4ec7d3)) 86 | 87 | ##### Documentation Changes 88 | 89 | * updated documentation refactoring some parts + explained bubbling order ([46527dc9](https://github.com/JointlyTech/object-loudifier/commit/46527dc9ef3b2a60af58edd76bd244304b01333b)) 90 | 91 | ##### New Features 92 | 93 | * internal refactor of once + internal refactor of on to allow for more options ([7945bdb8](https://github.com/JointlyTech/object-loudifier/commit/7945bdb8c3056178c4d70e63d037d083578e6077)) 94 | * added bubbling prevention ([1c51d074](https://github.com/JointlyTech/object-loudifier/commit/1c51d074d3a8a9a7dda07422a6270af38c262397)) 95 | 96 | ##### Bug Fixes 97 | 98 | * corrected infinite loop on ([11ba4c81](https://github.com/JointlyTech/object-loudifier/commit/11ba4c818a7c0921c127b18dc0786a6011c336a4)) 99 | * fixed test execution results ([daf19c3b](https://github.com/JointlyTech/object-loudifier/commit/daf19c3b9bd5bc09bca92f29e881592794f6b333)) 100 | 101 | ##### Other Changes 102 | 103 | * added tests to check bubbling ordering ([4920167a](https://github.com/JointlyTech/object-loudifier/commit/4920167afaadbe8c9fb32ee41197373eb87e4049)) 104 | 105 | #### 1.0.1 (2022-12-12) 106 | 107 | ##### Tests 108 | 109 | * fixed tests ([0853e3ca](https://github.com/JointlyTech/object-loudifier/commit/0853e3ca676461210fe58f9954c0fe684197e095)) 110 | 111 | ## 1.0.0 (2022-12-12) 112 | 113 | ##### Other Changes 114 | 115 | * changed exported function name + updated docs ([d48ae7f6](https://github.com/JointlyTech/object-loudifier/commit/d48ae7f68b3c086f52d43e030c630a5aa9e2354b)) 116 | 117 | #### 0.0.3 (2022-12-12) 118 | 119 | ##### Chores 120 | 121 | * added commitlint ([1cb0852c](https://github.com/JointlyTech/object-loudifier/commit/1cb0852cb7c29b9390533e85cfea1cc642d8be94)) 122 | 123 | #### 0.0.2 (2022-12-12) 124 | 125 | ##### Chores 126 | 127 | * new scaffolding ([f572572c](https://github.com/JointlyTech/object-loudifier/commit/f572572c99c738558b7d1fcdc18a56a8624f709a)) 128 | * Updating license, package and readme ([e89338ac](https://github.com/JointlyTech/object-loudifier/commit/e89338ac447416df95fe87a95fb00b004e12ad5b)) 129 | * Added tests to lint-staged ([20c26693](https://github.com/JointlyTech/object-loudifier/commit/20c266938ba78b45a5a6a3d7ccaa89121884b021)) 130 | 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jointly 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is it? 2 | 3 | This is a library allowing to create reactive objects. 4 | Given an object, it will return a new object with the same properties but with the ability to react to changes in the original object. 5 | 6 | # How do I install it? 7 | 8 | You can install it by using the following command: 9 | 10 | ```bash 11 | npm install @jointly/object-loudifier 12 | ``` 13 | 14 | # How to use it? 15 | 16 | Just pass any object to the `loudify` function and it will return a loud object. 17 | 18 | ```js 19 | import { loudify } from '@jointly/object-loudifier'; 20 | const obj = loudify({ 21 | a: { 22 | b: { 23 | c: { 24 | d: 1 25 | } 26 | } 27 | } 28 | }); 29 | obj.$on('*', (v) => { 30 | console.log('* -->', v); 31 | return; 32 | }); 33 | obj.$on('a.*', (v) => { 34 | console.log('a.* -->', v); 35 | return; 36 | }); 37 | obj.a.b = 1; // This will cause both console.logs to execute. 38 | ``` 39 | 40 | # Tests 41 | 42 | You can run the tests by using the following command: 43 | 44 | ```bash 45 | npm test 46 | ``` 47 | 48 | # How does it work? 49 | 50 | Just wrap the object in the `loudify` function. 51 | The function expects just a single parameter, the object you want to make reactive. 52 | You can then watch for changes in the object by using the `$on` method. 53 | The function expects three parameters: 54 | - The name of the property you want to watch 55 | - A callback function. 56 | - An `options` parameter for additional configuration. It is explained later in the document as `Listener options`. 57 | 58 | The callback function will be called every time the property changes. 59 | The callback function will be called with the new value of the property as its first parameter. 60 | You can also watch for changes for nested properties by using the dot notation and the wildcards (Explained in the `Wildcards` section). 61 | 62 | # Other Info 63 | 64 | ## Wildcards 65 | 66 | You can use wildcards to watch for changes in multiple properties. 67 | For example, if you want to watch for changes in the `foo` and `bar` properties, you can use the following code: 68 | 69 | ```js 70 | const obj = loudify({ foo: 1, bar: 2 }); 71 | obj.$on('*', (newValue) => { 72 | console.log(newValue); 73 | }); 74 | ``` 75 | 76 | You can also use wildcards for nested properties. 77 | 78 | ```js 79 | const obj = loudify({ foo: { bar: 1 } }, { allowNesting: true }); 80 | obj.$on('foo.*', (newValue) => { 81 | console.log(newValue); 82 | }); 83 | ``` 84 | 85 | ## Listener options 86 | 87 | You can pass a third parameter to the `$on` method, which is an object with the following properties: 88 | - `preventBubbling` - A boolean indicating if bubbling should be prevented. Default is `false`. 89 | - `once` - A boolean indicating if the listener should be called only once. Default is `false`. 90 | 91 | ```js 92 | obj.$on('foo.bar', (newValue) => { 93 | console.log(newValue); 94 | }, { preventBubbling: false, once: true }); 95 | ``` 96 | 97 | ### Bubbling 98 | 99 | By default, the `$on` method will bubble the changes to the parent object. 100 | For example, if you have the following object: 101 | 102 | ```js 103 | const obj = loudify({ foo: { bar: 1 } }, { allowNesting: true }); 104 | ``` 105 | 106 | And you watch for changes in the `foo.bar` property and in the `foo` property, you will get notified in both cases. 107 | 108 | ```js 109 | obj.$on('foo.bar', (newValue) => { 110 | console.log(newValue); 111 | }); 112 | 113 | obj.$on('foo', (newValue) => { 114 | console.log(newValue); 115 | }); 116 | ``` 117 | 118 | #### Bubbling priority 119 | 120 | The event emission order is the following: 121 | 1. The property listeners are called. 122 | 2. The parent listeners are called. 123 | 3. The wildcard listeners are called using the following sub-order: 124 | 1. The property listeners are called. 125 | 2. The parent listeners are called. 126 | 127 | Considering the following example: 128 | 129 | ```js 130 | const obj = loudify({ foo: { bar: 1 } }, { allowNesting: true }); 131 | obj.foo.$on('bar', (newValue) => { 132 | console.log('foo->bar'); 133 | }); 134 | obj.$on('*', (newValue) => { 135 | console.log('*'); 136 | }); 137 | obj.$on('foo.*', (newValue) => { 138 | console.log('foo.*'); 139 | }); 140 | obj.$on('foo.bar', (newValue) => { 141 | console.log('foo.bar'); 142 | }); 143 | ``` 144 | 145 | The following output will be printed: 146 | 147 | ```bash 148 | foo->bar 149 | foo.bar 150 | foo.* 151 | * 152 | ``` 153 | 154 | ### Once 155 | 156 | The `$once` method is a shortcut for the `$on` method with the `once` option set to `true`. 157 | 158 | # Benchmarks 159 | 160 | You can run the benchmarks by using the following command: 161 | 162 | ```bash 163 | npm run benchmark 164 | ``` 165 | 166 | Tested on a MacBook Pro M1 Max (Retina, 16-inch, 2021) with 32GB of RAM. 167 | The results are in milliseconds. 168 | The results are the average of 100000 runs. 169 | 170 | | Benchmark | Without loudify | With loudify | Notes | 171 | | --------- | --------------- | ------------ | -------------------------------------------------------------- | 172 | | 1 | 1.73 | 55.72 | Simple object assignment | 173 | | 2 | 1.38 | 144.11 | Object nested assignment | 174 | | 3 | 1.51 | 155.05 | Object nested assignment with wildcard | 175 | | 4 | 1.57 | 244.63 | Object nested assignment with wildcard and multiple listeners | 176 | | 5 | 1.54 | 721.68 | Object assignment with multiple nested properties | 177 | | 6 | 1.82 | 997.00 | Object assignment with multiple nested properties and wildcard | 178 | 179 | Even if the benchmarks show a big difference with a native object, yet the library is capable of easily handling tens of thousands of changes per second. 180 | 181 | In a real-case scenario, reaching milions of changes per second is possible (As in Benchmark #1). -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | 3 | const __DEV__ = process.env.NODE_ENV === 'development'; 4 | const __PROD__ = process.env.NODE_ENV === 'production'; 5 | 6 | // ESM - Currently disabled as CommonJS named exports seem to work pretty well with ESM based imports 7 | /*esbuild 8 | .build({ 9 | entryPoints: ['src/index.ts'], 10 | outdir: 'dist', 11 | bundle: true, 12 | sourcemap: true, 13 | minify: true, 14 | splitting: true, 15 | format: 'esm', 16 | target: ['esnext'] 17 | }) 18 | .catch(() => process.exit(1));*/ 19 | 20 | // CJS 21 | esbuild 22 | .build({ 23 | entryPoints: ['src/index.ts'], 24 | outfile: 'dist/index.js', 25 | format: 'cjs', 26 | bundle: true, 27 | sourcemap: __DEV__, 28 | minify: __PROD__, 29 | platform: 'node', 30 | // ???? 31 | // there's no node14.X option https://esbuild.github.io/api/#target 32 | target: ['node14.16'] 33 | }) 34 | .catch(() => process.exit(1)); 35 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jointly/object-loudifier", 3 | "private": false, 4 | "version": "2.1.2", 5 | "main": "dist/index.js", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "exports": { 12 | ".": { 13 | "import": "./dist/index.js", 14 | "require": "./dist/index.js" 15 | }, 16 | "./package.json": "./package.json" 17 | }, 18 | "scripts": { 19 | "ts-types": " tsc --emitDeclarationOnly --outDir dist", 20 | "check": "npm run prettier && npm run lint && npm test", 21 | "build": "npm run check && rimraf dist && NODE_ENV=production node esbuild.js && npm run ts-types", 22 | "demo-test": "for file in demo/*.js; do node $file; done", 23 | "prettier": "prettier --write ./src", 24 | "lint": "eslint ./src --ext .ts", 25 | "dev": "jest --watch", 26 | "test": "jest --no-cache", 27 | "coverage": "jest --coverage", 28 | "benchmark": "npx ts-node ./src/benchmark.ts", 29 | "release:common": "npm run build && git push --follow-tags origin main && npm publish --access public", 30 | "release:patch": "changelog -p && git add CHANGELOG.md && git commit -m 'docs: updated changelog' && npm version patch && npm run release:common", 31 | "release:minor": "changelog -m && git add CHANGELOG.md && git commit -m 'docs: updated changelog' && npm version minor && npm run release:common", 32 | "release:major": "changelog -M && git add CHANGELOG.md && git commit -m 'docs: updated changelog' && npm version major && npm run release:common" 33 | }, 34 | "prepare": "npm run build", 35 | "devDependencies": { 36 | "@commitlint/cli": "^17.3.0", 37 | "@commitlint/config-conventional": "^17.3.0", 38 | "@types/jest": "^29.2.4", 39 | "@types/node": "^18.11.12", 40 | "@typescript-eslint/eslint-plugin": "^5.46.0", 41 | "@typescript-eslint/parser": "^5.46.0", 42 | "esbuild": "^0.16.4", 43 | "eslint": "^8.29.0", 44 | "generate-changelog": "^1.8.0", 45 | "husky": "^8.0.2", 46 | "jest": "^29.3.1", 47 | "lint-staged": "^13.1.0", 48 | "prettier": "^2.8.1", 49 | "rimraf": "^3.0.2", 50 | "ts-jest": "^29.0.3", 51 | "typescript": "^4.9.4" 52 | }, 53 | "lint-staged": { 54 | "src/**/*.{js,jsx,ts,tsx}": [ 55 | "npx prettier --write", 56 | "npx eslint --fix" 57 | ] 58 | }, 59 | "repository": { 60 | "type": "git", 61 | "url": "git://github.com/JointlyTech/object-loudifier.git" 62 | }, 63 | "license": "MIT", 64 | "author": "Jointly " 65 | } 66 | -------------------------------------------------------------------------------- /src/benchmark.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { performance } from 'perf_hooks'; 4 | import { loudify } from './lib'; 5 | 6 | const iterations = 100000; 7 | 8 | console.log('Benchmarking loudify...'); 9 | console.log(`Iterations: ${iterations}`); 10 | 11 | /** BENCHMARK #1 - Simple object assignment */ 12 | console.log('Benchmark #1'); 13 | (() => { 14 | // Create an object 15 | const obj = { a: 0 }; 16 | 17 | const start = performance.now(); 18 | // Benchmark re-assigning obj.a `iterations` times 19 | for (let i = 0; i < iterations; i++) { 20 | obj.a = i; 21 | } 22 | 23 | const end = performance.now(); 24 | console.log(`Time taken (without loudify): ${end - start}ms`); 25 | })(); 26 | 27 | (() => { 28 | // Create a loud object 29 | const obj = loudify({ a: 0 }); 30 | 31 | obj.$on('a', () => { 32 | return; 33 | }); 34 | 35 | const start = performance.now(); 36 | // Benchmark re-assigning obj.a `iterations` times 37 | for (let i = 0; i < iterations; i++) { 38 | obj.a = i; 39 | } 40 | 41 | const end = performance.now(); 42 | console.log(`Time taken (with loudify): ${end - start}ms`); 43 | })(); 44 | 45 | /** BENCHMARK #2 - Object nested assignment */ 46 | console.log('Benchmark #2'); 47 | (() => { 48 | // Create an object 49 | const obj = { a: { b: 0 } }; 50 | 51 | const start = performance.now(); 52 | // Benchmark re-assigning obj.a.b `iterations` times 53 | for (let i = 0; i < iterations; i++) { 54 | obj.a.b = i; 55 | } 56 | 57 | const end = performance.now(); 58 | console.log(`Time taken (without loudify): ${end - start}ms`); 59 | })(); 60 | 61 | (() => { 62 | // Create a loud object 63 | const obj = loudify({ a: { b: 0 } }, { allowNesting: true }); 64 | 65 | obj.$on('a.b', () => { 66 | return; 67 | }); 68 | 69 | const start = performance.now(); 70 | // Benchmark re-assigning obj.a.b `iterations` times 71 | for (let i = 0; i < iterations; i++) { 72 | obj.a.b = i; 73 | } 74 | 75 | const end = performance.now(); 76 | console.log(`Time taken (with loudify): ${end - start}ms`); 77 | })(); 78 | 79 | /** BENCHMARK #3 - Object nested assignment with wildcard */ 80 | console.log('Benchmark #3'); 81 | (() => { 82 | // Create an object 83 | const obj = { a: { b: 0 } }; 84 | 85 | const start = performance.now(); 86 | // Benchmark re-assigning obj.a.b `iterations` times 87 | for (let i = 0; i < iterations; i++) { 88 | obj.a.b = i; 89 | } 90 | 91 | const end = performance.now(); 92 | console.log(`Time taken (without loudify): ${end - start}ms`); 93 | })(); 94 | 95 | (() => { 96 | // Create a loud object 97 | const obj = loudify({ a: { b: 0 } }, { allowNesting: true }); 98 | 99 | obj.$on('*', () => { 100 | return; 101 | }); 102 | 103 | const start = performance.now(); 104 | // Benchmark re-assigning obj.a.b `iterations` times 105 | for (let i = 0; i < iterations; i++) { 106 | obj.a.b = i; 107 | } 108 | 109 | const end = performance.now(); 110 | console.log(`Time taken (with loudify): ${end - start}ms`); 111 | })(); 112 | 113 | /** BENCHMARK #4 - Object nested assignment with wildcard and multiple listeners */ 114 | console.log('Benchmark #4'); 115 | (() => { 116 | // Create an object 117 | const obj = { a: { b: 0 } }; 118 | 119 | const start = performance.now(); 120 | // Benchmark re-assigning obj.a.b `iterations` times 121 | for (let i = 0; i < iterations; i++) { 122 | obj.a.b = i; 123 | } 124 | 125 | const end = performance.now(); 126 | console.log(`Time taken (without loudify): ${end - start}ms`); 127 | })(); 128 | 129 | (() => { 130 | // Create a loud object 131 | const obj = loudify({ a: { b: 0 } }, { allowNesting: true }); 132 | 133 | obj.$on('*', () => { 134 | return; 135 | }); 136 | 137 | obj.$on('a.b', () => { 138 | return; 139 | }); 140 | 141 | obj.$on('a.*', () => { 142 | return; 143 | }); 144 | 145 | obj.a.$on('*', () => { 146 | return; 147 | }); 148 | 149 | obj.a.$on('b', () => { 150 | return; 151 | }); 152 | 153 | const start = performance.now(); 154 | // Benchmark re-assigning obj.a.b `iterations` times 155 | for (let i = 0; i < iterations; i++) { 156 | obj.a.b = i; 157 | } 158 | 159 | const end = performance.now(); 160 | console.log(`Time taken (with loudify): ${end - start}ms`); 161 | })(); 162 | 163 | /** BENCHMARK #5 - Object assignment with multiple nested properties */ 164 | console.log('Benchmark #5'); 165 | (() => { 166 | // Create an object 167 | const obj = { a: { b: 0 }, c: { d: 0 }, e: { f: { g: { h: 1 } } } }; 168 | 169 | const start = performance.now(); 170 | // Benchmark re-assigning obj.a.b `iterations` times 171 | for (let i = 0; i < iterations; i++) { 172 | obj.a.b = i; 173 | obj.c.d = i; 174 | obj.e.f.g.h = i; 175 | } 176 | 177 | const end = performance.now(); 178 | console.log(`Time taken (without loudify): ${end - start}ms`); 179 | })(); 180 | 181 | (() => { 182 | // Create a loud object 183 | const obj = loudify( 184 | { a: { b: 0 }, c: { d: 0 }, e: { f: { g: { h: 1 } } } }, 185 | { allowNesting: true } 186 | ); 187 | 188 | obj.$on('a.b', () => { 189 | return; 190 | }); 191 | 192 | obj.$on('c.d', () => { 193 | return; 194 | }); 195 | 196 | obj.$on('e.f.g.h', () => { 197 | return; 198 | }); 199 | 200 | const start = performance.now(); 201 | // Benchmark re-assigning obj.a.b `iterations` times 202 | for (let i = 0; i < iterations; i++) { 203 | obj.a.b = i; 204 | obj.c.d = i; 205 | obj.e.f.g.h = i; 206 | } 207 | 208 | const end = performance.now(); 209 | console.log(`Time taken (with loudify): ${end - start}ms`); 210 | })(); 211 | 212 | /** BENCHMARK #6 - Object assignment with multiple nested properties and wildcard */ 213 | console.log('Benchmark #6'); 214 | (() => { 215 | // Create an object 216 | const obj = { a: { b: 0 }, c: { d: 0 }, e: { f: { g: { h: 1 } } } }; 217 | 218 | const start = performance.now(); 219 | // Benchmark re-assigning obj.a.b `iterations` times 220 | for (let i = 0; i < iterations; i++) { 221 | obj.a.b = i; 222 | obj.c.d = i; 223 | obj.e.f.g.h = i; 224 | } 225 | 226 | const end = performance.now(); 227 | console.log(`Time taken (without loudify): ${end - start}ms`); 228 | })(); 229 | 230 | (() => { 231 | // Create a loud object 232 | const obj = loudify( 233 | { a: { b: 0 }, c: { d: 0 }, e: { f: { g: { h: 1 } } } }, 234 | { allowNesting: true } 235 | ); 236 | 237 | obj.$on('*', () => { 238 | return; 239 | }); 240 | 241 | const start = performance.now(); 242 | // Benchmark re-assigning obj.a.b `iterations` times 243 | for (let i = 0; i < iterations; i++) { 244 | obj.a.b = i; 245 | obj.c.d = i; 246 | obj.e.f.g.h = i; 247 | } 248 | 249 | const end = performance.now(); 250 | console.log(`Time taken (with loudify): ${end - start}ms`); 251 | })(); 252 | -------------------------------------------------------------------------------- /src/defaults.ts: -------------------------------------------------------------------------------- 1 | import { $onOptions, Options } from './models'; 2 | 3 | export const defaultOptions: Options = { 4 | allowNesting: false 5 | }; 6 | 7 | export const $onDefaultOptions: $onOptions = { 8 | preventBubbling: false, 9 | once: false 10 | }; 11 | -------------------------------------------------------------------------------- /src/example.md: -------------------------------------------------------------------------------- 1 | ```js 2 | import { performance } from 'perf_hooks'; 3 | import { loudify } from '.'; 4 | 5 | (async () => { 6 | const obj = loudify({ 7 | a: { 8 | b: { 9 | c: { 10 | d: 1 11 | } 12 | } 13 | } 14 | }); 15 | obj.$on('*', (v) => { 16 | console.log('* -->', v); 17 | return; 18 | }); 19 | obj.$on('a.*', (v) => { 20 | console.log('a.* -->', v); 21 | return; 22 | }); 23 | obj.a.b = 1; 24 | obj.a.b = 2; 25 | const loudObj = loudify({ 26 | nest2: { 27 | nest3: { 28 | nest4: { 29 | nest5: { 30 | nest6: 'valorenest6' 31 | } 32 | } 33 | } 34 | } 35 | }); 36 | loudObj.$on('nest2.nest3.nest4.nest5.nest6', (value) => { 37 | console.log('FROM root -->\t nest2.nest3.nest4.nest5.nest6', value); 38 | }); 39 | loudObj.nest2.nest3.$on('nest4.nest5.nest6', (value) => { 40 | console.log('FROM nest3 -->\t nest2.nest3.nest4.nest5.nest6', value); 41 | }); 42 | loudObj.nest2.nest3.nest4.nest5.nest6 = 'valorenest6modificato'; 43 | loudObj.laterNest2 = { 44 | laterNest3: { 45 | laterNest4: 'laterNest4Value' 46 | } 47 | }; 48 | loudObj.$on('laterNest2.laterNest3.laterNest4', (value) => { 49 | console.log('FROM root -->\t laterNest2.laterNest3.laterNest4', value); 50 | }); 51 | loudObj.laterNest2.laterNest3.$on('laterNest4', (value) => { 52 | console.log('FROM nest3 -->\t laterNest2.laterNest3.laterNest4', value); 53 | }); 54 | loudObj.laterNest2.laterNest3.laterNest4 = 'laterNest4ValueModified'; 55 | // Create a new reactive object, then listen for the * event. 56 | const obj2 = loudify({}); 57 | obj2.$on('*', (v) => { 58 | return; 59 | }); 60 | // Now mutate the object 1000 times and monitor the performance. 61 | const start = performance.now(); 62 | for (let i = 0; i < 10000; i++) { 63 | obj2[i] = i; 64 | } 65 | const end = performance.now(); 66 | console.log('10000 mutations took', end - start, 'ms'); 67 | })(); 68 | ``` 69 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | // Create tests using Jest 2 | 3 | import { loudify } from './lib'; 4 | 5 | it('should create a loud object', () => { 6 | const obj = loudify({}); 7 | expect(obj.$isLoud).toBe(true); 8 | }); 9 | 10 | it('should throw if the object is not an object', () => { 11 | expect(() => loudify(1)).toThrow(); 12 | }); 13 | 14 | it('should throw if the object is an array', () => { 15 | expect(() => loudify([])).toThrow(); 16 | }); 17 | 18 | it('should throw if the object is null', () => { 19 | expect(() => loudify(null)).toThrow(); 20 | }); 21 | 22 | it('should throw if the object contains a reserved property', () => { 23 | expect(() => 24 | loudify({ 25 | $on: () => { 26 | return; 27 | } 28 | }) 29 | ).toThrow(); 30 | }); 31 | 32 | it('should create a loud object for every property of the object', () => { 33 | const obj = loudify( 34 | { 35 | a: { 36 | b: 1 37 | } 38 | }, 39 | { allowNesting: true } 40 | ); 41 | expect(obj.a.$isLoud).toBe(true); 42 | }); 43 | 44 | it('should not create a loud object for every property of the object if allowNesting is false', () => { 45 | const obj = loudify({ 46 | a: { 47 | b: 1 48 | } 49 | }); 50 | expect(obj.a.$isLoud).toBe(undefined); 51 | }); 52 | 53 | it('should emit when a property is set', () => { 54 | const obj = loudify({}); 55 | const callback = jest.fn(); 56 | obj.$on('a', callback); 57 | obj.a = 1; 58 | expect(callback).toBeCalledWith(1, 'a', expect.anything()); 59 | }); 60 | 61 | it('should emit when a new property, which is an object, is added and modified', () => { 62 | const obj = loudify({}, { allowNesting: true }); 63 | const callback = jest.fn(); 64 | obj.$on('a', callback); 65 | obj.a = { b: { c: 1 } }; 66 | expect(callback).toBeCalledWith({ b: { c: 1 } }, 'a', expect.anything()); 67 | const callback2 = jest.fn(); 68 | obj.$on('a.b', callback2); 69 | obj.a.b = { c: 3 }; 70 | expect(callback).toBeCalledWith({ b: { c: 3 } }, 'a', expect.anything()); 71 | }); 72 | 73 | it('should only emit once if you use $once', () => { 74 | const obj = loudify({}); 75 | const callback = jest.fn(); 76 | obj.$once('a', callback); 77 | obj.a = 1; 78 | obj.a = 2; 79 | expect(callback).toBeCalledWith(1, 'a', expect.anything()); 80 | expect(callback).toBeCalledTimes(1); 81 | }); 82 | 83 | it('should emit the correct amount of times when a wildcard is used', () => { 84 | const obj = loudify( 85 | { 86 | a: { 87 | b: { 88 | c: { 89 | d: 1 90 | } 91 | } 92 | } 93 | }, 94 | { allowNesting: true } 95 | ); 96 | const callback = jest.fn(); 97 | obj.$on('*', callback); 98 | obj.a.b.c.d = 2; 99 | expect(callback).toBeCalledTimes(1); 100 | obj.$on('a.b.c', callback); 101 | obj.$on('a.b.*', callback); 102 | obj.$on('a.*', callback); 103 | obj.a.b.c.d = 2; 104 | expect(callback).toBeCalledTimes(4); 105 | }); 106 | 107 | it('should not emit in case I off', () => { 108 | const obj = loudify({}); 109 | const callback = jest.fn(); 110 | obj.$on('a', callback); 111 | obj.$off('a', callback); 112 | obj.a = 1; 113 | expect(callback).not.toBeCalled(); 114 | }); 115 | 116 | it('should preventBubbling', () => { 117 | const obj = loudify( 118 | { 119 | a: { 120 | b: { 121 | c: 1 122 | } 123 | } 124 | }, 125 | { allowNesting: true } 126 | ); 127 | const callback = jest.fn(); 128 | obj.$on('a.b.c', callback, { 129 | preventBubbling: true 130 | }); 131 | obj.$on('a.b.*', callback); 132 | obj.a.b.c = 2; 133 | expect(callback).toBeCalledTimes(1); 134 | }); 135 | 136 | it('should respect the bubbling order', () => { 137 | const order: Array = []; 138 | const obj = loudify( 139 | { 140 | a: { 141 | b: { 142 | c: 1 143 | } 144 | } 145 | }, 146 | { allowNesting: true } 147 | ); 148 | const callback1 = function () { 149 | order.push(1); 150 | }; 151 | 152 | const callback2 = function () { 153 | order.push(2); 154 | }; 155 | 156 | const callback3 = function () { 157 | order.push(3); 158 | }; 159 | 160 | obj.a.b.$on('c', callback1); 161 | obj.$on('a.b.c', callback2); 162 | obj.$on('*', callback3); 163 | obj.a.b.c = 2; 164 | expect(order).toEqual([1, 2, 3]); 165 | }); 166 | 167 | it('should return the correct dirtiness information via metadata', () => { 168 | const obj = loudify( 169 | { 170 | a: { 171 | b: { 172 | c: 1 173 | } 174 | } 175 | }, 176 | { allowNesting: true } 177 | ); 178 | obj.$on('*', (value, prop, metadata) => { 179 | expect(metadata.isDirty).toBe(true); 180 | }); 181 | obj.a.$on('*', (value, prop, metadata) => { 182 | expect(metadata.isDirty).toBe(true); 183 | }); 184 | obj.a.b.c = 2; 185 | 186 | const obj2 = loudify( 187 | { 188 | a: { 189 | b: { 190 | c: 1 191 | } 192 | } 193 | }, 194 | { allowNesting: true } 195 | ); 196 | obj2.$on('*', (value, prop, metadata) => { 197 | expect(metadata.isDirty).toBe(false); 198 | }); 199 | obj2.a.$on('*', (value, prop, metadata) => { 200 | expect(metadata.isDirty).toBe(false); 201 | }); 202 | obj2.a.b.c = 1; 203 | }); 204 | 205 | it('should return correct property name when using wildcards', () => { 206 | const obj = loudify( 207 | { 208 | a: { 209 | b: { 210 | c: 1 211 | } 212 | } 213 | }, 214 | { allowNesting: true } 215 | ); 216 | obj.$on('*', (value, prop) => { 217 | expect(prop).toBe('a.b.c'); 218 | }); 219 | obj.a.$on('*', (value, prop) => { 220 | expect(prop).toBe('b.c'); 221 | }); 222 | obj.a.b.$on('*', (value, prop) => { 223 | expect(prop).toBe('c'); 224 | }); 225 | obj.a.b.c = 2; 226 | }); 227 | 228 | it('should return the object if already loud', () => { 229 | const obj = loudify({}); 230 | const loudObj = loudify(obj); 231 | expect(loudObj).toBe(obj); 232 | }); 233 | 234 | it('should throw exception when calling on with a non-string', () => { 235 | const obj = loudify({}); 236 | expect(() => { 237 | obj.$on(1 as any, () => { 238 | return; 239 | }); 240 | }).toThrow(); 241 | }); 242 | 243 | it('should throw exception when calling on with a reserved property', () => { 244 | const obj = loudify({}); 245 | expect(() => { 246 | obj.$on('$on', () => { 247 | return; 248 | }); 249 | }).toThrow(); 250 | }); 251 | 252 | it('should throw exception when calling a nested listener while allowNesting is false', () => { 253 | const obj = loudify({}); 254 | expect(() => { 255 | obj.$on('a.b', () => { 256 | return; 257 | }); 258 | }).toThrow(); 259 | }); 260 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | export * from './models'; 3 | export * from './defaults'; 4 | -------------------------------------------------------------------------------- /src/lib.ts: -------------------------------------------------------------------------------- 1 | import { defaultOptions, $onDefaultOptions } from './defaults'; 2 | import { 3 | emittedMetadata, 4 | ListenerFn, 5 | Options, 6 | $onOptions, 7 | LoudObject 8 | } from './models'; 9 | 10 | const RESERVED_PROPERTIES = new Set([ 11 | '$on', 12 | '$off', 13 | '$once', 14 | '$emit', 15 | '$isLoud', 16 | '$listeners', 17 | '$parent', 18 | '$propName', 19 | '$preventBubbling' 20 | ]); 21 | 22 | export const loudify = ( 23 | obj: any, 24 | options: Partial = {}, 25 | parent: Object | undefined = undefined, 26 | propName: string | undefined = undefined 27 | ): LoudObject => { 28 | options = { ...defaultOptions, ...options }; 29 | 30 | // If the object is not an object, throw 31 | if (typeof obj !== 'object' || Array.isArray(obj) || obj === null) { 32 | throw new Error('The object passed to loudify() must be an object'); 33 | } 34 | 35 | // If the object contains one of the reserved properties, throw 36 | if (Object.keys(obj).some((key) => RESERVED_PROPERTIES.has(key))) { 37 | throw new Error( 38 | `The 39 | object passed to loudify() cannot contain any of the following properties: ${Array.from( 40 | RESERVED_PROPERTIES 41 | ).join(', ')}` 42 | ); 43 | } 44 | 45 | // If the object is already loud, return it 46 | if (obj.$isLoud) { 47 | return obj; 48 | } 49 | 50 | // Create a loud object for every property of the object if an object itself 51 | if (options.allowNesting) { 52 | applyLoudifyToNestedProperties(); 53 | } 54 | 55 | const loudObj = new Proxy(obj, { 56 | set: (target, prop, value) => { 57 | const metadata: emittedMetadata = { 58 | isDirty: target[prop] !== value 59 | }; 60 | target[prop] = value; 61 | 62 | // If prop is a symbol or is a prop of the loudObj, return, nothing to do. 63 | if (typeof prop === 'symbol' || RESERVED_PROPERTIES.has(prop)) 64 | return true; 65 | 66 | // If value is an object, create a loud object for it. 67 | if (typeof value === 'object') { 68 | value = loudify(value, options, target, prop); 69 | } 70 | 71 | // If value is not a function, emit the event. 72 | if (typeof value !== 'function') { 73 | loudObj.$emit(prop, value, metadata); 74 | } 75 | return true; 76 | } 77 | }); 78 | 79 | loudObj.$isLoud = true; 80 | loudObj.$parent = parent; 81 | loudObj.$propName = propName; 82 | //loudObj.$listeners = {} as Record; 83 | loudObj.$listeners = new Map() as Map; 84 | loudObj.$preventBubbling = false; 85 | 86 | loudObj.$on = ( 87 | prop: unknown, 88 | listener: ListenerFn, 89 | onOptions: Partial<$onOptions> = {} 90 | ) => { 91 | // If prop is not a string, throw 92 | if (typeof prop !== 'string') { 93 | throw new Error('The first argument to $on() must be a string'); 94 | } 95 | 96 | // If prop is a reserved property, throw 97 | if (RESERVED_PROPERTIES.has(prop)) { 98 | throw new Error( 99 | `Cannot listen to ${prop} as it is a reserved property and could potentially create an infinite loop.` 100 | ); 101 | } 102 | 103 | onOptions = { ...$onDefaultOptions, ...onOptions }; 104 | 105 | // If propr contains a wildcard and allowNesting is false, throw 106 | if (/\./.test(prop) && !options.allowNesting) { 107 | throw new Error( 108 | 'Cannot listen to a nested event if allowNesting is false' 109 | ); 110 | } 111 | 112 | const initialListener = listener; 113 | // If once is true, create a new listener that will remove itself after being called 114 | if (onOptions.once) { 115 | listener = createOnceListener(initialListener, prop); 116 | } 117 | // If preventBubbling is true, create a new listener that will prevent the event from bubbling 118 | if (onOptions.preventBubbling) { 119 | listener = createPreventBubblingListener(initialListener); 120 | } 121 | 122 | createListenersForPropIfNotExists(prop); 123 | pushListenerForProp(prop, listener); 124 | }; 125 | loudObj.$once = (prop: string, listener: ListenerFn) => { 126 | loudObj.$on(prop, listener, { once: true }); 127 | }; 128 | loudObj.$emit = (prop: string, value: unknown, metadata: emittedMetadata) => { 129 | if (hasListenersForProp(prop)) { 130 | getListenersForProp(prop).forEach((listen: ListenerFn) => 131 | listen(value, metadata.originalPropertyName || prop, metadata) 132 | ); 133 | } 134 | 135 | // If the event was prevented from bubbling, return 136 | if (loudObj.$preventBubbling) { 137 | loudObj.$preventBubbling = false; 138 | return; 139 | } 140 | 141 | if (loudObj.$parent) { 142 | const parentPropName = `${loudObj.$propName}.${prop}`; 143 | loudObj.$parent.$emit(parentPropName, value, metadata); 144 | } 145 | if (!prop.includes('*')) { 146 | emitWildcardEventForEachParentMatchingExpression(prop, value, metadata); 147 | } 148 | }; 149 | loudObj.$off = (prop: string, listener: ListenerFn) => { 150 | if (hasListenersForProp(prop)) { 151 | setListenersForProp( 152 | prop, 153 | getListenersForProp(prop).filter((l: ListenerFn) => l !== listener) 154 | ); 155 | } 156 | }; 157 | 158 | // Prevent the RESERVED_PROPERTIES from being exposed 159 | Array.from(RESERVED_PROPERTIES).forEach((prop) => { 160 | Object.defineProperty(loudObj, prop, { 161 | enumerable: false, 162 | writable: true 163 | }); 164 | }); 165 | 166 | return loudObj; 167 | 168 | function hasListenersForProp(prop: string) { 169 | //return loudObj.$listeners[prop] && loudObj.$listeners[prop].length > 0; 170 | return ( 171 | loudObj.$listeners.has(prop) && loudObj.$listeners.get(prop).length > 0 172 | ); 173 | } 174 | 175 | function getListenersForProp(prop: string) { 176 | //return loudObj.$listeners[prop]; 177 | return loudObj.$listeners.get(prop); 178 | } 179 | 180 | function setListenersForProp(prop: string, listeners: ListenerFn[]) { 181 | //loudObj.$listeners[prop] = listeners; 182 | loudObj.$listeners.set(prop, listeners); 183 | } 184 | 185 | function applyLoudifyToNestedProperties() { 186 | Object.keys(obj).forEach((key) => { 187 | if (typeof obj[key] === 'object') { 188 | obj[key] = loudify(obj[key], options, obj, key); 189 | } 190 | }); 191 | } 192 | 193 | function emitWildcardEventForEachParentMatchingExpression( 194 | prop: string, 195 | value: unknown, 196 | metadata: emittedMetadata 197 | ) { 198 | const propParts = prop.split('.'); 199 | for (let i = propParts.length; i > 0; i--) { 200 | const wildcardProp = propParts.slice(0, i).join('.') + '.*'; 201 | //if (loudObj.$listeners[wildcardProp]) { 202 | if (loudObj.$listeners.has(wildcardProp)) { 203 | loudObj.$emit(wildcardProp, value, { 204 | ...metadata, 205 | originalPropertyName: prop 206 | }); 207 | } 208 | } 209 | //if (loudObj.$listeners['*']) { 210 | if (loudObj.$listeners.has('*')) { 211 | loudObj.$emit('*', value, { 212 | ...metadata, 213 | originalPropertyName: prop 214 | }); 215 | } 216 | } 217 | 218 | function createPreventBubblingListener(initialListener) { 219 | return function newListener(...args: unknown[]) { 220 | initialListener(...args); 221 | loudObj.$preventBubbling = true; 222 | }; 223 | } 224 | 225 | function createOnceListener(initialListener: ListenerFn, prop: string) { 226 | return function newListener(...args: unknown[]) { 227 | initialListener(...args); 228 | loudObj.$off(prop, newListener); 229 | }; 230 | } 231 | 232 | function createListenersForPropIfNotExists(prop: string) { 233 | /*if (!loudObj.$listeners[prop]) { 234 | loudObj.$listeners[prop] = []; 235 | }*/ 236 | if (!loudObj.$listeners.has(prop)) { 237 | loudObj.$listeners.set(prop, []); 238 | } 239 | } 240 | 241 | function pushListenerForProp(prop: string, listener: ListenerFn) { 242 | //loudObj.$listeners[prop].push(listener); 243 | loudObj.$listeners.get(prop).push(listener); 244 | } 245 | }; 246 | -------------------------------------------------------------------------------- /src/models.ts: -------------------------------------------------------------------------------- 1 | export type Options = { 2 | allowNesting: boolean; 3 | }; 4 | 5 | export type $onOptions = { 6 | preventBubbling: boolean; 7 | once: boolean; 8 | }; 9 | 10 | export type emittedMetadata = { 11 | isDirty: boolean; 12 | originalPropertyName?: string; 13 | }; 14 | 15 | export type ListenerFn = ( 16 | value?: unknown, 17 | prop?: string, 18 | metadata?: emittedMetadata 19 | ) => void; 20 | 21 | export type LoudObject = T & { 22 | $isLoud: true; 23 | $on: ( 24 | prop: string, 25 | listener: ListenerFn, 26 | options: Partial<$onOptions> 27 | ) => void; 28 | $once: (prop: string, listener: ListenerFn) => void; 29 | $emit: (prop: string, value: unknown, metadata: emittedMetadata) => void; 30 | $off: (prop: string, listener: ListenerFn) => void; 31 | $parent?: LoudObject; 32 | $propName?: string; 33 | $listeners: Record; 34 | $preventBubbling: boolean; 35 | }; 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "esnext", 5 | "lib": ["esnext", "dom"], 6 | "strict": true, 7 | "noImplicitAny": false, 8 | "removeComments": true, 9 | "esModuleInterop": true, 10 | "moduleResolution": "node", 11 | "outDir": "dist" 12 | }, 13 | "exclude": ["node_modules", "**/*.test.js", "**/*.test.ts", "dist"] 14 | } 15 | --------------------------------------------------------------------------------