├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── bench.js ├── circle.yml ├── index.js ├── karma.conf.js ├── package.json └── test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | 10 | "rules": { 11 | "strict": 2, 12 | "valid-jsdoc": 0, 13 | 14 | "default-case": 2, 15 | "no-self-compare": 2, 16 | "no-else-return": 2, 17 | "no-throw-literal": 2, 18 | "no-void": 2, 19 | 20 | "max-params": [1, 3], 21 | "max-depth": [1, 3], 22 | "max-len": [1, 100, 2], 23 | "indent": [1, 2], 24 | 25 | "comma-style": [2, "last"], 26 | "brace-style": [2, "1tbs", { "allowSingleLine": false }], 27 | "consistent-this": [2, "DO NOT USE"], 28 | 29 | "space-in-brackets": [2, "never"], 30 | "space-before-function-parentheses": [2, "never"], 31 | "space-before-blocks": [2, "always"], 32 | "space-after-keywords": [2, "always"], 33 | 34 | "no-var": 0, 35 | "no-new-require": 2, 36 | "no-lonely-if": 2, 37 | "no-nested-ternary": 2, 38 | "no-multiple-empty-lines": [2, { 39 | "max": 1 40 | }], 41 | "no-underscore-dangle": 0, 42 | "no-unused-expressions": 0, 43 | "no-use-before-define": 0, 44 | "quotes": [2, "single"], 45 | "no-multi-spaces": [2, { 46 | "exceptions": { 47 | "Property": true, 48 | "ImportDeclaration": true, 49 | "VariableDeclarator": true, 50 | "AssignmentExpression": true 51 | } 52 | }], 53 | "key-spacing": [2, { 54 | "align": "colon", 55 | "beforeColon": true, 56 | "afterColon": true 57 | }] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/changed.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 "ahomu" Ayumu Sato 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rx.Observable.combineTemplate 2 | 3 | [![npm version][npm-image]][npm-url] [![build status][circle-image]][circle-url] [![Dependency Status][deps-image]][deps-url] 4 | 5 | Generate values based on the Observable and object template. Similar to [`Bacon.combineTemplate`](https://github.com/baconjs/bacon.js#observable-combine). 6 | 7 | ## Usage 8 | 9 | ### Install 10 | 11 | ```bash 12 | npm install --save rx.observable.combinetemplate 13 | ``` 14 | 15 | ### Basics 16 | 17 | ```javascript 18 | import combineTemplate from 'rx.observable.combinetemplate'; 19 | import * as Rx from 'rx'; 20 | 21 | let subject1 = new Rx.Subject(); 22 | let subject2 = new Rx.Subject(); 23 | 24 | combineTemplate({ 25 | foo : 'bar', 26 | baz : { 27 | foo : ['bar', subject1 'qux'] 28 | }, 29 | qux : { 30 | foo : { 31 | foo : 'bar' 32 | baz : subject2 33 | } 34 | } 35 | }).subscribe((value)=> { 36 | console.log(value); 37 | /* === output === 38 | { 39 | foo : 'bar', 40 | baz : { 41 | foo : ['bar', 'BAZ' 'qux'] 42 | }, 43 | qux : { 44 | foo : { 45 | foo : 'bar' 46 | baz : 'QUX' 47 | } 48 | } 49 | } 50 | */ 51 | }); 52 | 53 | subject1.onNext('BAZ'); 54 | subject2.onNext('QUX'); 55 | ``` 56 | 57 | ### with React 58 | 59 | State is updated automatically receives a value from the observables. 60 | 61 | ```javascript 62 | componentWillMount() { 63 | combineTemplate({ 64 | items : store.itemsObservable$, 65 | count : store.itemsObservable$.map((items) => items.length) 66 | }).subscribe(this.setState.bind(this)); 67 | } 68 | ``` 69 | 70 | ## Tests 71 | 72 | ``` 73 | npm test 74 | ``` 75 | 76 | ## Contributing 77 | 78 | 1. Fork it! 79 | 2. Create your feature branch: `git checkout -b my-new-feature` 80 | 3. Commit your changes: `git commit -am 'Add some feature'` 81 | 4. Push to the branch: `git push origin my-new-feature` 82 | 5. Submit a pull request :D 83 | 84 | ## License 85 | 86 | MIT 87 | 88 | [npm-image]: https://img.shields.io/npm/v/rx.observable.combinetemplate.svg 89 | [npm-url]: https://npmjs.org/package/rx.observable.combinetemplate 90 | [circle-image]: https://circleci.com/gh/ahomu/rx.observable.combinetemplate.svg?style=shield&circle-token=b12ab2a48027a249724e0b1924ccec8152d3068a 91 | [circle-url]: https://circleci.com/gh/ahomu/rx.observable.combinetemplate 92 | [deps-image]: https://david-dm.org/ahomu/rx.observable.combinetemplate.svg 93 | [deps-url]: https://david-dm.org/ahomu/rx.observable.combinetemplate 94 | -------------------------------------------------------------------------------- /bench.js: -------------------------------------------------------------------------------- 1 | var Benchmark = require('benchmark'); 2 | var Rx = require('rx-lite'); 3 | var before = require('./src/index'); 4 | var after = require('./src/changed'); 5 | 6 | var suite = new Benchmark.Suite(); 7 | 8 | // add tests 9 | suite.add('before', {defer: true, fn: function(deferred) { 10 | var subject1 = new Rx.Subject(); 11 | var subject2 = new Rx.Subject(); 12 | var subject3 = new Rx.Subject(); 13 | 14 | var observable = before({ 15 | foo : 'bar', 16 | baz : { 17 | foo : { 18 | foo : subject1 19 | }, 20 | bar : 'baz' 21 | }, 22 | qux : { 23 | foo : [1, subject2, 3], 24 | baz : subject3 25 | } 26 | }); 27 | 28 | observable.subscribe(function(v) { 29 | if (v != null) { 30 | deferred.resolve(); 31 | } 32 | }); 33 | 34 | subject1.onNext('foo'); 35 | subject2.onNext('bar'); 36 | subject3.onNext('qux'); 37 | }}) 38 | .add('after', {defer: true, fn: function(deferred) { 39 | var subject1 = new Rx.Subject(); 40 | var subject2 = new Rx.Subject(); 41 | var subject3 = new Rx.Subject(); 42 | 43 | var observable = after({ 44 | foo : 'bar', 45 | baz : { 46 | foo : { 47 | foo : subject1 48 | }, 49 | bar : 'baz' 50 | }, 51 | qux : { 52 | foo : [1, subject2, 3], 53 | baz : subject3 54 | } 55 | }); 56 | 57 | observable.subscribe(function(v) { 58 | if (v != null) { 59 | deferred.resolve(); 60 | } 61 | }); 62 | 63 | subject1.onNext('foo'); 64 | subject2.onNext('bar'); 65 | subject3.onNext('qux'); 66 | }}) 67 | .on('cycle', function(event) { 68 | console.log(String(event.target)); 69 | }) 70 | .on('complete', function() { 71 | console.log('Fastest is ' + this.filter('fastest').pluck('name')); 72 | }) 73 | .run({ 'async': true }); 74 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | override: 3 | - npm test 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var clone = require('clone-deep'); 4 | var Rx = require('rx-lite'); 5 | 6 | /** 7 | * Generate values based on the Observable and object template. 8 | * Similar to `Bacon.combineTemplate`. 9 | * 10 | * ``` 11 | * var combineTemplate = require('rx.observable.combinetemplate') 12 | * 13 | * // observables 14 | * var password, username, firstname, lastname; 15 | * 16 | * // combine and publish structure! 17 | * var loginInfo = combineTemplate({ 18 | * magicNumber: 3, 19 | * userid: username, 20 | * passwd: password, 21 | * name: { first: firstname, last: lastname } 22 | * }); 23 | * 24 | * loginInfo.subscribe((v) => { 25 | * console.log(v); 26 | * }); 27 | * ``` 28 | * 29 | * @param {Object} templateObject 30 | * @returns {Rx.Observable} 31 | */ 32 | function combineTemplate(templateObject) { 33 | templateObject = templateObject || {}; 34 | 35 | // TODO avoid clone `Rx.Observable` 36 | var clonedTemplate = clone(templateObject); 37 | var collections = collectTargetObservablesAndContext(templateObject); 38 | 39 | return Rx.Observable.combineLatest( 40 | collections.targets, 41 | createCombineObserver(collections.contexts, clonedTemplate) 42 | ); 43 | } 44 | 45 | /** 46 | * @param {Array>} targetContexts 47 | * @returns {Function} 48 | */ 49 | function createCombineObserver(targetContexts, baseObject) { 50 | var prevValues = []; 51 | var returnObject = baseObject; 52 | 53 | /** 54 | * produce object that observer function. 55 | * 56 | * @param {...Array<*>} values 57 | * @return Object 58 | */ 59 | return function() { 60 | var newValues = Array.prototype.slice.call(arguments); 61 | 62 | // Compares the `newValues` and `prevValues` to confirm position has changed 63 | var changedArgPositions = newValues.map(function(value, i) { 64 | return prevValues.indexOf(i) !== value ? i : null; 65 | }); 66 | 67 | // To update only the changed arguments 68 | changedArgPositions.forEach(function(i) { 69 | var newChangedArg = newValues[i]; 70 | var targetContext = targetContexts[i].slice(); 71 | var target = returnObject; 72 | 73 | // Continuous updating references to the one before the end of the `targetContext` 74 | while (targetContext.length > 1) { 75 | target = target[targetContext.shift()]; 76 | } 77 | 78 | target[targetContext.shift()] = newChangedArg; 79 | }); 80 | 81 | prevValues = newValues.slice(); 82 | return returnObject; 83 | }; 84 | } 85 | 86 | /** 87 | * Log target observable & context that indicates the position in the object. 88 | * 89 | * @param {Object} templateObject 90 | * @returns {{targets: Array, contexts: Array}} 91 | */ 92 | function collectTargetObservablesAndContext(templateObject) { 93 | var targets = []; 94 | var contexts = []; 95 | 96 | /** 97 | * 98 | * ``` 99 | * // context index sample (`x` == Observable) 100 | * { 101 | * foo: x, // => ['foo'] 102 | * bar: { 103 | * foo: x, // => ['bar', 'foo'] 104 | * bar: [_, _, x] // => ['bar', 'bar', 2] 105 | * }, 106 | * baz: [_, x, _], // => ['baz', 1] 107 | * qux: { 108 | * foo: { 109 | * foo: x // => ['qux', 'foo', 'foo'] 110 | * } 111 | * } 112 | * } 113 | * ``` 114 | * 115 | * @param {Array<*>|Object<*>} list 116 | * @param {Array>} parentContext like [0, 3, 2...] 117 | */ 118 | function walker(list, parentContext) { 119 | 120 | if (Array.isArray(list)) { 121 | list.forEach(evaluator); 122 | } else { 123 | Object.keys(list).forEach(function(key) { 124 | evaluator(list[key], key); 125 | }); 126 | } 127 | 128 | function evaluator(value, key) { 129 | var context = parentContext.slice(); 130 | context.push(key); 131 | 132 | // maybe isObservable 133 | if (value != null && value.subscribe != null && value.publish != null) { 134 | targets.push(value); 135 | contexts.push(context); 136 | 137 | // isArray || isObject 138 | } else if (Array.isArray(value) || (!!value && typeof value === 'object')) { 139 | walker(value, context); 140 | } 141 | } 142 | } 143 | 144 | walker(templateObject, []); 145 | 146 | return { 147 | targets : targets, 148 | contexts : contexts 149 | }; 150 | } 151 | 152 | module.exports = combineTemplate; 153 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | frameworks : ['browserify', 'mocha'], 6 | 7 | files : [ 8 | 'test.js' 9 | ], 10 | 11 | preprocessors : { 12 | 'test.js' : 'browserify' 13 | }, 14 | 15 | browserify : { 16 | transform : [ 17 | require('babelify').configure({ 18 | stage : 0, 19 | plugins : ['babel-plugin-espower'] 20 | }) 21 | ], 22 | debug : true, 23 | extensions : ['.js'] 24 | }, 25 | 26 | browsers : ['Chrome', 'Firefox'], 27 | 28 | autoWatch : true, 29 | 30 | reporters : ['dots'] 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rx.observable.combinetemplate", 3 | "version": "0.2.6", 4 | "description": "Generate values based on the Observable and object template. Similar to `Bacon.combineTemplate`", 5 | "author": "ahomu", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "npm run tdd", 9 | "test": "karma start --singleRun", 10 | "tdd": "karma start", 11 | "lint": "eslint ./src", 12 | "patch": "npm version patch -m \"bump v%s !\"", 13 | "minor": "npm version minor -m \"bump v%s !!\"", 14 | "major": "npm version major -m \"bump v%s !!!\"" 15 | }, 16 | "files": [ 17 | "index.js", 18 | "README.md", 19 | "package.json" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/ahomu/rx.observable.combinetemplate" 24 | }, 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/ahomu/rx.observable.combinetemplate/issues" 28 | }, 29 | "homepage": "https://github.com/ahomu/rx.observable.combinetemplate", 30 | "dependencies": { 31 | "clone-deep": "^0.2.0" 32 | }, 33 | "peerDependencies": { 34 | "rx-lite": "^2.5.2" 35 | }, 36 | "devDependencies": { 37 | "babel-core": "^5.4.3", 38 | "babel-eslint": "^3.1.1", 39 | "babel-plugin-espower": "^0.4.1", 40 | "babelify": "^6.1.0", 41 | "benchmark": "^1.0.0", 42 | "browserify": "^9.0.3", 43 | "eslint": "^0.17.1", 44 | "karma": "^0.12.31", 45 | "karma-browserify": "^4.0.0", 46 | "karma-chrome-launcher": "^0.1.7", 47 | "karma-cli": "0.0.4", 48 | "karma-firefox-launcher": "^0.1.6", 49 | "karma-mocha": "^0.1.10", 50 | "mocha": "^2.2.1", 51 | "power-assert": "^0.11.0", 52 | "rx-lite": "^2.5.2", 53 | "watchify": "^2.4.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import assert from 'power-assert'; 4 | import combineTemplate from './'; 5 | import Rx from 'rx-lite'; 6 | 7 | describe('rx.observable.combineTemplate', ()=> { 8 | 9 | it('empty', () => { 10 | let combined = combineTemplate({}); 11 | combined.subscribe(() => {}); 12 | }); 13 | 14 | it('falsy value', () => { 15 | let combined = combineTemplate({ 16 | foo : null, 17 | bar : undefined 18 | }); 19 | combined.subscribe(() => {}); 20 | }); 21 | 22 | it('object', (done) => { 23 | let subject = new Rx.Subject(); 24 | let combined = combineTemplate({ 25 | foo : 'bar', 26 | test : subject 27 | }); 28 | 29 | combined.subscribe((v) => { 30 | if (v.test != null) { 31 | assert(v.foo === 'bar'); 32 | assert(v.test === 'baz'); 33 | done(); 34 | } 35 | }); 36 | 37 | subject.onNext('baz'); 38 | }); 39 | 40 | it('array', (done) => { 41 | let subject1 = new Rx.Subject(); 42 | let observable = new Rx.Observable.just('bar'); 43 | let subject2 = new Rx.Subject(); 44 | 45 | let combined = combineTemplate({ 46 | test : [subject1, observable, subject2], 47 | qux : 'c⌒っ.ω.)っ' 48 | }); 49 | 50 | combined.subscribe((v) => { 51 | if (v != null) { 52 | assert(v.test[0] === 'foo'); 53 | assert(v.test[1] === 'bar'); 54 | assert(v.test[2] === 'baz'); 55 | assert(v.qux === 'c⌒っ.ω.)っ'); 56 | done(); 57 | } 58 | }); 59 | 60 | subject1.onNext('foo'); 61 | subject2.onNext('baz'); 62 | }); 63 | 64 | it('nested', (done) => { 65 | let subject1 = new Rx.Subject(); 66 | let observable = new Rx.Observable.just('bar'); 67 | let subject2 = new Rx.Subject(); 68 | 69 | let combined = combineTemplate({ 70 | foo : 'bar', 71 | baz : { 72 | foo : { 73 | foo : subject1 74 | }, 75 | bar : 'baz' 76 | }, 77 | qux : { 78 | foo : [1, observable, 3], 79 | baz : subject2 80 | } 81 | }); 82 | 83 | combined.subscribe((v) => { 84 | if (v != null) { 85 | assert(v.foo === 'bar'); 86 | assert(v.baz.foo.foo === 'foo'); 87 | assert(v.baz.bar === 'baz'); 88 | assert(v.qux.foo[0] === 1); 89 | assert(v.qux.foo[1] === 'bar'); 90 | assert(v.qux.foo[2] === 3); 91 | assert(v.qux.baz === 'qux'); 92 | done(); 93 | } 94 | }); 95 | 96 | subject1.onNext('foo'); 97 | subject2.onNext('qux'); 98 | }); 99 | 100 | it('twice', (done) => { 101 | let subject = new Rx.Subject(); 102 | let observable = new Rx.Observable.just('FOO'); 103 | 104 | let combined = combineTemplate({ 105 | test : ['foo', subject, 'baz'], 106 | qux : observable 107 | }); 108 | 109 | combined.subscribe((v) => { 110 | if (v != null && v.test[1] === 'BAR') { 111 | assert(v.test[0] === 'foo'); 112 | assert(v.test[1] === 'BAR'); 113 | assert(v.test[2] === 'baz'); 114 | assert(v.qux === 'FOO'); 115 | subject.onNext('END'); 116 | } 117 | if (v != null && v.test[1] === 'END') { 118 | assert(v.test[0] === 'foo'); 119 | assert(v.test[1] === 'END'); 120 | assert(v.test[2] === 'baz'); 121 | assert(v.qux === 'FOO'); 122 | done(); 123 | } 124 | }); 125 | 126 | subject.onNext('BAR'); 127 | }); 128 | }); 129 | --------------------------------------------------------------------------------