├── .circleci └── config.yml ├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── CHANGELOG.md ├── LICENSE ├── README.md ├── karma.conf.js ├── package-dist.json ├── package.json ├── scripts └── pack.js ├── source ├── index.ts ├── observe-spec.ts └── observe.ts ├── tsconfig-dist-cjs.json ├── tsconfig-dist-esm2015.json ├── tsconfig-dist-esm5.json ├── tsconfig-dist.json ├── tsconfig.json ├── tslint.json ├── webpack.config.js ├── webpack.config.test.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | # https://circleci.com/docs/2.0/docker-image-tags.json 6 | - image: circleci/node:current-browsers 7 | steps: 8 | - checkout 9 | - run: 10 | name: Install Greenkeeper Lockfile 11 | command: | 12 | echo 'export PATH=$(yarn global bin):$PATH' >> $BASH_ENV 13 | source $BASH_ENV 14 | yarn global add greenkeeper-lockfile@1 15 | - run: 16 | name: Update Greenkeeper Lockfile 17 | command: "greenkeeper-lockfile-update" 18 | - restore_cache: 19 | name: Restore Yarn Package Cache 20 | keys: 21 | - yarn-packages-{{ checksum "yarn.lock" }} 22 | - run: 23 | name: Install Packages 24 | command: yarn install 25 | - save_cache: 26 | name: Save Yarn Package Cache 27 | key: yarn-packages-{{ checksum "yarn.lock" }} 28 | paths: 29 | - ~/.cache/yarn 30 | - run: 31 | name: Test 32 | command: yarn test 33 | - run: 34 | name: Upload Greenkeeper Lockfile 35 | command: "greenkeeper-lockfile-upload" 36 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | indent_size = 4 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [cartant] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /bundles 3 | /dist 4 | /node_modules 5 | /temp 6 | yarn-error.log 7 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [2.1.6](https://github.com/cartant/rxjs-observe/compare/v2.1.5...v2.1.6) (2021-05-19) 3 | 4 | ### Fixes 5 | 6 | * Widen RxJS peer range to support version 7. ([f9a7936](https://github.com/cartant/rxjs-observe/commit/f9a7936)) 7 | 8 | 9 | ## [2.1.5](https://github.com/cartant/rxjs-observe/compare/v2.1.4...v2.1.5) (2019-10-26) 10 | 11 | ### Fixes 12 | 13 | * Ensure the same function is returned for each property get. ([f9a7936](https://github.com/cartant/rxjs-observe/commit/f9a7936)) 14 | 15 | 16 | ## [2.1.4](https://github.com/cartant/rxjs-observe/compare/v2.1.3...v2.1.4) (2019-04-22) 17 | 18 | ### Changes 19 | 20 | * Export the `Observables` type. ([27fd8b3](https://github.com/cartant/rxjs-observe/commit/27fd8b3)) 21 | 22 | 23 | ## [2.1.3](https://github.com/cartant/rxjs-observe/compare/v2.1.2...v2.1.3) (2019-04-21) 24 | 25 | ### Changes 26 | 27 | * Lookup callback and target values - in the `get` handler - only once. ([4e73a89](https://github.com/cartant/rxjs-observe/commit/4e73a89)) 28 | 29 | 30 | ## [2.1.2](https://github.com/cartant/rxjs-observe/compare/v2.1.1...v2.1.2) (2019-04-21) 31 | 32 | ### Fixes 33 | 34 | * Add callbacks to proxy type. ([e19f466](https://github.com/cartant/rxjs-observe/commit/e19f466)) 35 | 36 | 37 | ## [2.1.1](https://github.com/cartant/rxjs-observe/compare/v2.1.0...v2.1.1) (2019-04-21) 38 | 39 | ### Fixes 40 | 41 | * Don't override instance methods with proxy callbacks. ([156439b](https://github.com/cartant/rxjs-observe/commit/156439b)) 42 | 43 | 44 | ## [2.1.0](https://github.com/cartant/rxjs-observe/compare/v2.0.0...v2.1.0) (2019-04-21) 45 | 46 | ### Features 47 | 48 | * Add proxy callbacks. ([f2b6bd5](https://github.com/cartant/rxjs-observe/commit/f2b6bd5)) 49 | 50 | 51 | ## [2.0.0](https://github.com/cartant/rxjs-observe/compare/v1.0.2...v2.0.0) (2018-11-19) 52 | 53 | ### Breaking Changes 54 | 55 | * Strongly type function parameters - which requires TypeScript 3.0 or later. ([e0c298f](https://github.com/cartant/rxjs-observe/commit/e0c298f)) 56 | 57 | 58 | ## [1.0.2](https://github.com/cartant/rxjs-observe/compare/v1.0.1...v1.0.2) (2018-06-15) 59 | 60 | ### Bug Fixes 61 | 62 | * Support `Symbol` properties and methods. ([af889b0](https://github.com/cartant/rxjs-observe/commit/af889b0)) 63 | 64 | 65 | ## [1.0.1](https://github.com/cartant/rxjs-observe/compare/v1.0.0...v1.0.1) (2018-06-14) 66 | 67 | ### Bug Fixes 68 | 69 | * Emit notifications after the assignment or call. ([af65aab](https://github.com/cartant/rxjs-observe/commit/af65aab)) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Nicholas Jamieson 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 | # rxjs-observe 2 | 3 | [![GitHub License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/cartant/rxjs-observe/blob/master/LICENSE) 4 | [![NPM version](https://img.shields.io/npm/v/rxjs-observe.svg)](https://www.npmjs.com/package/rxjs-observe) 5 | [![Build status](https://img.shields.io/travis/cartant/rxjs-observe.svg)](http://travis-ci.org/cartant/rxjs-observe) 6 | [![dependency status](https://img.shields.io/david/cartant/rxjs-observe.svg)](https://david-dm.org/cartant/rxjs-observe) 7 | [![devDependency Status](https://img.shields.io/david/dev/cartant/rxjs-observe.svg)](https://david-dm.org/cartant/rxjs-observe#info=devDependencies) 8 | [![peerDependency Status](https://img.shields.io/david/peer/cartant/rxjs-observe.svg)](https://david-dm.org/cartant/rxjs-observe#info=peerDependencies) 9 | 10 | ### What is it? 11 | 12 | It's an `observe` function that can be used to create observable sources for an arbitrary object's property assignements and method calls. 13 | 14 | ### Why might you need it? 15 | 16 | If you need to convert an imperative API to an observable API, you might find this useful. 17 | 18 | ## Install 19 | 20 | Install the package using NPM: 21 | 22 | ``` 23 | npm install rxjs-observe --save 24 | ``` 25 | 26 | TypeScript 3.0 or later is required, as the type declaration for `observe` uses [generic rest parameters](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#generic-rest-parameters). 27 | 28 | ## Usage 29 | 30 | Pass an object instance to `observe` and receive an `observables` object - that contains observable sources for the object's properties and methods - and a `proxy`: 31 | 32 | ```ts 33 | import { observe } from "rxjs-observe"; 34 | 35 | const instance = { name: "Alice" }; 36 | const { observables, proxy } = observe(instance); 37 | observables.name.subscribe(name => console.log(name)); 38 | proxy.name = "Bob"; 39 | ``` 40 | 41 | `observe` can be passed optional callbacks that will be implemented in the proxy - the observed instance does not need to implement them - and forwarded to an observable with the same name: 42 | 43 | ```ts 44 | import { callback, observe } from "rxjs-observe"; 45 | 46 | const instance = { name: "Alice" }; 47 | const { observables, proxy } = observe(instance, { 48 | init: callback() 49 | }); 50 | observables.init.subscribe(() => console.log("init")); 51 | proxy.init(); 52 | ``` 53 | 54 | `observe` can be called inside a constructor and the `proxy` can be returned, as in this Angular component: 55 | 56 | ```ts 57 | import { Component, Input, OnInit, OnDestroy } from "@angular/core"; 58 | import { switchMapTo, takeUntil } from "rxjs/operators"; 59 | import { callback, observe } from "rxjs-observe"; 60 | 61 | @Component({ 62 | selector: "some-component", 63 | template: "Some useless component that writes to the console" 64 | }) 65 | class SomeComponent { 66 | @Input() public name: string; 67 | constructor() { 68 | const { observables, proxy } = observe(this as SomeComponent, { 69 | ngOnInit: callback(), 70 | ngOnDestroy: callback() 71 | }); 72 | observables.ngOnInit.pipe( 73 | switchMapTo(observables.name), 74 | takeUntil(observables.ngOnDestroy) 75 | ).subscribe(value => console.log(value)); 76 | return proxy; 77 | } 78 | } 79 | ``` 80 | 81 | However, such a component implementation is ... unconventional, so proceed with caution, but ... YOLO. 82 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | process.env.CHROME_BIN = require("puppeteer").executablePath(); 4 | 5 | exports = module.exports = function(config) { 6 | config.set({ 7 | basePath: "", 8 | browsers: ["ChromeHeadlessNoSandbox"], 9 | colors: true, 10 | concurrency: Infinity, 11 | customLaunchers: { 12 | ChromeHeadlessNoSandbox: { 13 | base: "ChromeHeadless", 14 | flags: ["--no-sandbox"] 15 | } 16 | }, 17 | exclude: [], 18 | files: [{ pattern: "source/**/*-spec.ts", watched: false }], 19 | frameworks: ["mocha"], 20 | logLevel: config.LOG_INFO, 21 | mime: { 22 | "text/x-typescript": ["ts"] 23 | }, 24 | port: 9876, 25 | preprocessors: { 26 | "source/**/*-spec.ts": ["webpack"] 27 | }, 28 | proxies: {}, 29 | reporters: ["spec"], 30 | webpack: require("./webpack.config.test")({}), 31 | webpackMiddleware: { 32 | noInfo: true, 33 | stats: "errors-only" 34 | } 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /package-dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": {}, 3 | "es2015": "./esm2015/index.js", 4 | "main": "./index.js", 5 | "module": "./esm5/index.js", 6 | "private": false, 7 | "scripts": {}, 8 | "types": "./index.d.ts", 9 | "unpkg": "./bundles/rxjs-observe.min.umd.js" 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Nicholas Jamieson ", 3 | "bugs": { 4 | "url": "https://github.com/cartant/rxjs-observe/issues" 5 | }, 6 | "dependencies": {}, 7 | "description": "A library for observing an object's property assignments and method calls", 8 | "devDependencies": { 9 | "@cartant/tslint-config": "^2.0.0", 10 | "@cartant/tslint-config-etc": "^2.0.0", 11 | "@cartant/tslint-config-rxjs": "^2.0.0", 12 | "@types/chai": "^4.0.0", 13 | "@types/mocha": "^8.0.0", 14 | "@types/node": "^15.0.0", 15 | "chai": "^4.0.0", 16 | "cpy-cli": "^3.0.0", 17 | "husky": "^6.0.0", 18 | "karma": "^5.0.0", 19 | "karma-chrome-launcher": "^3.1.0", 20 | "karma-mocha": "^2.0.0", 21 | "karma-spec-reporter": "^0.0.32", 22 | "karma-webpack": "^4.0.2", 23 | "lint-staged": "^11.0.0", 24 | "mkdirp": "^1.0.0", 25 | "mocha": "^8.0.0", 26 | "prettier": "^2.1.2", 27 | "puppeteer": "^9.0.0", 28 | "rimraf": "^3.0.0", 29 | "rxjs": "^7.0.0", 30 | "rxjs-tslint-rules": "^4.0.0", 31 | "ts-loader": "^8.0.0", 32 | "ts-node": "^9.0.0", 33 | "tslint": "^6.0.0", 34 | "tslint-etc": "^1.2.0", 35 | "typescript": "~4.2.4", 36 | "webpack": "^4.0.0", 37 | "webpack-cli": "^3.0.0", 38 | "webpack-rxjs-externals": "^2.0.0" 39 | }, 40 | "es2015": "./dist/esm2015/index.js", 41 | "homepage": "https://github.com/cartant/rxjs-observe", 42 | "keywords": [ 43 | "observable", 44 | "rxjs" 45 | ], 46 | "license": "MIT", 47 | "lint-staged": { 48 | "*.{js,jsx,ts,tsx}": [ 49 | "prettier --write" 50 | ] 51 | }, 52 | "main": "./dist/index.js", 53 | "module": "./dist/esm5/index.js", 54 | "name": "rxjs-observe", 55 | "optionalDependencies": {}, 56 | "peerDependencies": { 57 | "rxjs": "^6.0.0 || ^7.0.0" 58 | }, 59 | "private": true, 60 | "publishConfig": { 61 | "tag": "latest" 62 | }, 63 | "repository": { 64 | "type": "git", 65 | "url": "https://github.com/cartant/rxjs-observe.git" 66 | }, 67 | "scripts": { 68 | "dist": "yarn run dist:build && yarn run dist:copy", 69 | "dist:build": "yarn run dist:clean && yarn run dist:build:cjs && yarn run dist:build:esm2015 && yarn run dist:build:esm5 && yarn run dist:build:bundle", 70 | "dist:build:bundle": "webpack --config webpack.config.js && webpack --config webpack.config.js --env.production", 71 | "dist:build:cjs": "tsc -p tsconfig-dist-cjs.json", 72 | "dist:build:esm2015": "tsc -p tsconfig-dist-esm2015.json", 73 | "dist:build:esm5": "tsc -p tsconfig-dist-esm5.json", 74 | "dist:clean": "rimraf dist && rimraf bundles/rxjs-observe.* && mkdirp bundles", 75 | "dist:copy": "node scripts/pack.js && cpy bundles/rxjs-observe.* dist/bundles/ && cpy CHANGELOG.md LICENSE README.md dist/", 76 | "lint": "tslint --project tsconfig.json source/**/*.ts", 77 | "prepare": "husky install", 78 | "prettier": "prettier --write \"./**/{scripts,source}/**/*.{js,json,ts}\"", 79 | "prettier:ci": "prettier --check \"./**/{scripts,source}/**/*.{js,json,ts}\"", 80 | "test": "yarn run lint && yarn run test:build && yarn run test:mocha && yarn run test:karma", 81 | "test:build": "yarn run test:clean && tsc -p tsconfig.json", 82 | "test:clean": "rimraf build", 83 | "test:karma": "karma start --single-run", 84 | "test:mocha": "mocha build/**/*-spec.js", 85 | "test:watch": "yarn run lint && yarn run test:build && karma start" 86 | }, 87 | "types": "./dist/index.d.ts", 88 | "unpkg": "./bundles/rxjs-observe.min.umd.js", 89 | "version": "2.1.6" 90 | } 91 | -------------------------------------------------------------------------------- /scripts/pack.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-observe 4 | */ 5 | 6 | "use strict"; 7 | 8 | const fs = require("fs"); 9 | 10 | const content = Object.assign( 11 | {}, 12 | JSON.parse(fs.readFileSync("./package.json")), 13 | JSON.parse(fs.readFileSync("./package-dist.json")) 14 | ); 15 | fs.writeFileSync("./dist/package.json", JSON.stringify(content, null, 2)); 16 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-observe 4 | */ 5 | 6 | export * from "./observe"; 7 | -------------------------------------------------------------------------------- /source/observe-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-observe 4 | */ 5 | /*tslint:disable:no-unused-expression rxjs-no-ignored-subscription rxjs-no-unsafe-scope*/ 6 | 7 | import { expect } from "chai"; 8 | import { Observable } from "rxjs"; 9 | import { callback, observe } from "./observe"; 10 | 11 | describe("observe", () => { 12 | describe("outside a constructor", () => { 13 | const job = Symbol("job"); 14 | class Person { 15 | [job]: string; 16 | constructor(public age: number, public name: string) {} 17 | greet(greeting: string): string { 18 | return `${greeting}, ${this.name}.`; 19 | } 20 | } 21 | 22 | it("should observe properties", () => { 23 | const person = new Person(32, "Alice"); 24 | const { observables, proxy } = observe(person); 25 | const values: (number | string)[] = []; 26 | observables.age.subscribe((value) => values.push(value)); 27 | observables.name.subscribe((value) => values.push(value)); 28 | expect(values).to.deep.equal([32, "Alice"]); 29 | proxy.age = 42; 30 | proxy.name = "Bob"; 31 | expect(values).to.deep.equal([32, "Alice", 42, "Bob"]); 32 | }); 33 | 34 | it("should observe functions", () => { 35 | const person = new Person(32, "Alice"); 36 | const { observables, proxy } = observe(person); 37 | const calls: any[][] = []; 38 | observables.greet.subscribe((args) => calls.push(args)); 39 | expect(calls).to.deep.equal([]); 40 | proxy.greet("Hi"); 41 | expect(calls).to.deep.equal([["Hi"]]); 42 | }); 43 | 44 | it("should replay properties", () => { 45 | const person = new Person(32, "Alice"); 46 | const { observables, proxy } = observe(person); 47 | proxy.age = 42; 48 | proxy.name = "Bob"; 49 | const values: (number | string)[] = []; 50 | observables.age.subscribe((value) => values.push(value)); 51 | observables.name.subscribe((value) => values.push(value)); 52 | expect(values).to.deep.equal([42, "Bob"]); 53 | }); 54 | 55 | it("should not replay functions", () => { 56 | const person = new Person(32, "Alice"); 57 | const { observables, proxy } = observe(person); 58 | proxy.greet("Hi"); 59 | const calls: any[][] = []; 60 | observables.greet.subscribe((args) => calls.push(args)); 61 | expect(calls).to.deep.equal([]); 62 | }); 63 | 64 | it("should support symbols", () => { 65 | const person = new Person(32, "Alice"); 66 | person[job] = "engineer"; 67 | const { observables } = observe(person); 68 | observables[job].subscribe((value) => expect(value).to.equal("engineer")); 69 | }); 70 | 71 | it("should return the same function for each property get", () => { 72 | const person = new Person(32, "Alice"); 73 | const first = person.greet; 74 | const second = person.greet; 75 | expect(first).to.equal(second); 76 | }); 77 | }); 78 | 79 | describe("inside a constructor", () => { 80 | class Person { 81 | age$: Observable; 82 | name$: Observable; 83 | greet$: Observable<[string]>; 84 | constructor(public age: number, public name: string) { 85 | const { observables, proxy } = observe(this as Person); 86 | this.age$ = observables.age; 87 | this.name$ = observables.name; 88 | this.greet$ = observables.greet; 89 | return proxy; 90 | } 91 | greet(greeting: string): string { 92 | return `${greeting}, ${this.name}.`; 93 | } 94 | } 95 | 96 | it("should observe properties", () => { 97 | const person = new Person(32, "Alice"); 98 | expect(person).to.have.property("age$"); 99 | expect(person).to.have.property("name$"); 100 | const values: (number | string)[] = []; 101 | person.age$.subscribe((value) => values.push(value)); 102 | person.name$.subscribe((value) => values.push(value)); 103 | expect(values).to.deep.equal([32, "Alice"]); 104 | person.age = 42; 105 | person.name = "Bob"; 106 | expect(values).to.deep.equal([32, "Alice", 42, "Bob"]); 107 | }); 108 | 109 | it("should observe functions", () => { 110 | const person = new Person(32, "Alice"); 111 | expect(person).to.have.property("greet$"); 112 | const calls: any[][] = []; 113 | person.greet$.subscribe((args) => calls.push(args)); 114 | expect(calls).to.deep.equal([]); 115 | person.greet("Hi"); 116 | expect(calls).to.deep.equal([["Hi"]]); 117 | }); 118 | }); 119 | 120 | describe("added callbacks", () => { 121 | class Component { 122 | onDestroy$: Observable<[]>; 123 | onInit$: Observable<[]>; 124 | constructor() { 125 | const { observables, proxy } = observe(this as Component, { 126 | onDestroy: callback<() => void>(), 127 | onInit: callback<() => void>(), 128 | }); 129 | this.onDestroy$ = observables.onDestroy; 130 | this.onInit$ = observables.onInit; 131 | return proxy; 132 | } 133 | } 134 | 135 | it("should observe added methods", () => { 136 | const component = new Component(); 137 | 138 | expect(Object.getOwnPropertyNames(component)).to.include("onInit"); 139 | expect(component.hasOwnProperty("onInit")).to.be.true; 140 | expect("onInit" in component).to.be.true; 141 | expect(component).to.have.property("onInit"); 142 | 143 | expect(Object.getOwnPropertyNames(component)).to.include("onDestroy"); 144 | expect(component.hasOwnProperty("onDestroy")).to.be.true; 145 | expect("onDestroy" in component).to.be.true; 146 | expect(component).to.have.property("onDestroy"); 147 | 148 | let initialized = false; 149 | let destroyed = false; 150 | component.onInit$.subscribe(() => (initialized = true)); 151 | component.onDestroy$.subscribe(() => (destroyed = true)); 152 | component["onInit"](); 153 | component["onDestroy"](); 154 | expect(initialized).to.be.true; 155 | expect(destroyed).to.be.true; 156 | }); 157 | 158 | it("should return the same function for each property get", () => { 159 | const component = new Component(); 160 | const first = component["onInit"]; 161 | const second = component["onInit"]; 162 | expect(first).to.equal(second); 163 | }); 164 | 165 | it("should not override instance methods", () => { 166 | const instance = { 167 | init() { 168 | /*tslint:disable-next-line:no-invalid-this*/ 169 | this.initialized = true; 170 | }, 171 | initialized: false, 172 | }; 173 | const { observables, proxy } = observe(instance, { 174 | init: callback(), 175 | }); 176 | let initialized = false; 177 | observables.init.subscribe(() => (initialized = true)); 178 | proxy.init(); 179 | expect(instance).to.have.property("initialized", true); 180 | expect(initialized).to.be.true; 181 | }); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /source/observe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/rxjs-observe 4 | */ 5 | 6 | import { BehaviorSubject, Observable, Subject } from "rxjs"; 7 | 8 | export type Observables = { 9 | [K in keyof T]: T[K] extends (...args: infer U) => any 10 | ? Observable 11 | : Observable; 12 | } & 13 | { 14 | [K in keyof C]: C[K] extends (...args: infer U) => any 15 | ? Observable 16 | : Observable; 17 | }; 18 | 19 | export function observe( 20 | instance: T, 21 | callbacks?: C 22 | ): { 23 | observables: Observables; 24 | proxy: T & C; 25 | } { 26 | const fallbacks: {} = callbacks || {}; 27 | const functions = new Map(); 28 | const subjects = new Map>(); 29 | const proxy = new Proxy(instance, { 30 | get(target: any, name: string | symbol) { 31 | const fallbackValue = fallbacks[name]; 32 | const targetValue = target[name]; 33 | let value = fallbackValue && !targetValue ? fallbackValue : targetValue; 34 | if (typeof value === "function") { 35 | const functionValue = value; 36 | let functionWrapper = functions.get(functionValue); 37 | if (!functionWrapper) { 38 | functionWrapper = function (this: any, ...args: any[]): any { 39 | const result = functionValue.apply(this, args); 40 | const subject = subjects.get(name); 41 | if (subject) { 42 | subject.next(args); 43 | } 44 | return result; 45 | }; 46 | functions.set(functionValue, functionWrapper); 47 | } 48 | value = functionWrapper; 49 | } 50 | return value; 51 | }, 52 | getOwnPropertyDescriptor(target: any, name: string | symbol) { 53 | return ( 54 | Object.getOwnPropertyDescriptor(target, name) || 55 | Object.getOwnPropertyDescriptor(fallbacks, name) 56 | ); 57 | }, 58 | has(target: any, name: string | symbol) { 59 | return name in target || name in fallbacks; 60 | }, 61 | ownKeys(target: any) { 62 | return [...Reflect.ownKeys(target), ...Reflect.ownKeys(fallbacks)]; 63 | }, 64 | set(target: any, name: string | symbol, value: any) { 65 | target[name] = value; 66 | const subject = subjects.get(name); 67 | if (subject) { 68 | subject.next(value); 69 | } 70 | return true; 71 | }, 72 | }); 73 | return { 74 | observables: new Proxy( 75 | {}, 76 | { 77 | get(target: any, name: string | symbol): any { 78 | let subject = subjects.get(name); 79 | if (!subject) { 80 | subject = 81 | typeof instance[name] === "function" || 82 | typeof fallbacks[name] === "function" 83 | ? new Subject() 84 | : new BehaviorSubject(instance[name]); 85 | subjects.set(name, subject); 86 | } 87 | return subject.asObservable(); 88 | }, 89 | } 90 | ), 91 | proxy, 92 | }; 93 | } 94 | 95 | export function callback(): T { 96 | return (() => {}) as any; 97 | } 98 | -------------------------------------------------------------------------------- /tsconfig-dist-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "target": "es5" 7 | }, 8 | "extends": "./tsconfig-dist.json" 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig-dist-esm2015.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "module": "es2015", 5 | "outDir": "dist/esm2015", 6 | "target": "es2015" 7 | }, 8 | "extends": "./tsconfig-dist.json" 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig-dist-esm5.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "module": "es2015", 5 | "outDir": "dist/esm5", 6 | "target": "es5" 7 | }, 8 | "extends": "./tsconfig-dist.json" 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig-dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "source/**/*-spec.ts" 4 | ], 5 | "extends": "./tsconfig.json" 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "importHelpers": false, 5 | "lib": ["es2017"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noEmitHelpers": false, 9 | "noImplicitAny": true, 10 | "outDir": "build", 11 | "removeComments": true, 12 | "skipLibCheck": true, 13 | "sourceMap": false, 14 | "strict": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "target": "es2015" 17 | }, 18 | "exclude": [], 19 | "include": [ 20 | "source/**/*.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "@cartant/tslint-config", 5 | "@cartant/tslint-config-etc", 6 | "@cartant/tslint-config-rxjs" 7 | ], 8 | "rules": { 9 | "rxjs-no-create": { 10 | "severity": "off" 11 | }, 12 | "typedef": { 13 | "options": [ 14 | "parameter", 15 | "property-declaration" 16 | ], 17 | "severity": "error" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const webpack = require("webpack"); 5 | const webpackRxjsExternals = require("webpack-rxjs-externals"); 6 | 7 | module.exports = env => { 8 | let filename = "rxjs-observe.umd.js"; 9 | let mode = "development"; 10 | if (env && env.production) { 11 | filename = "rxjs-observe.min.umd.js"; 12 | mode = "production"; 13 | } 14 | return { 15 | context: path.join(__dirname, "./"), 16 | entry: { 17 | index: "./source/index.ts" 18 | }, 19 | externals: webpackRxjsExternals(), 20 | mode, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.ts$/, 25 | use: { 26 | loader: "ts-loader", 27 | options: { 28 | compilerOptions: { 29 | declaration: false 30 | }, 31 | configFile: "tsconfig-dist-cjs.json" 32 | } 33 | } 34 | } 35 | ] 36 | }, 37 | output: { 38 | filename, 39 | library: "rxjsObserve", 40 | libraryTarget: "umd", 41 | path: path.resolve(__dirname, "./bundles") 42 | }, 43 | resolve: { 44 | extensions: [".ts", ".js"] 45 | } 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /webpack.config.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const webpack = require("webpack"); 5 | 6 | module.exports = env => { 7 | return { 8 | context: path.join(__dirname, "./"), 9 | mode: "development", 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.ts$/, 14 | use: { 15 | loader: "ts-loader", 16 | options: { 17 | compilerOptions: { 18 | declaration: false 19 | }, 20 | configFile: "tsconfig.json" 21 | } 22 | } 23 | } 24 | ] 25 | }, 26 | resolve: { 27 | extensions: [".ts", ".js"] 28 | } 29 | }; 30 | }; 31 | --------------------------------------------------------------------------------