├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.d.ts ├── karma.conf.js ├── package-lock.json ├── package.json ├── rollup ├── rollup.base.config.js ├── rollup.es.config.js ├── rollup.umd.config.js └── rollup.umd.min.config.js ├── src ├── connect.js ├── index.js └── store.js └── tests ├── .eslintrc ├── connect.spec.js └── store.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-rollup"], 3 | "plugins": ["transform-object-assign"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "spreetail", 3 | "parserOptions": { 4 | "sourceType": "module" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist 4 | lib 5 | es 6 | knockout-store-1.0.0.tgz 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased] 9 | 10 | - None 11 | 12 | ## [4.0.0] - 2020-05-02 13 | 14 | ### Changed 15 | 16 | - **BREAKING**: Constrain Knockout peer dependency versions to ^3.5.0 17 | - Remove @types/knockout dependency 18 | - Update TypeScript declarations to use new Knockout types 19 | 20 | ## [3.0.2] - 2019-02-08 21 | 22 | ### Fixed 23 | 24 | - Fix package vulnerabilities that GitHub alerted us to 25 | 26 | ## [3.0.1] - 2017-12-06 27 | 28 | ### Fixed 29 | 30 | - Include TypeScript declaration file with package contents. 31 | 32 | ## [3.0.0] - 2017-12-06 33 | 34 | ### Added 35 | 36 | - TypeScript declaration file. 37 | 38 | ### Changed 39 | 40 | - `connect` throws an error if `mapStateToParams` or `mergeParams` are not `null` or a function. 41 | - If `mapStateToParams` or `mergeParams` are `null`, the respective default will be used. 42 | - Calling the result of `connect()` without a function (view model) will throw an `Error` instead of a `TypeError`. 43 | 44 | ## [2.0.0] - 2017-09-20 45 | 46 | ### Removed 47 | 48 | - CommonJS export (covered by UMD export) 49 | 50 | ### Changed 51 | 52 | - pkg.module now exports transpiled code 53 | 54 | ## [1.0.2] - 2017-05-05 55 | 56 | ### Fixed 57 | 58 | - README.md links 59 | 60 | ## [1.0.1] - 2017-05-05 61 | 62 | ### Changed 63 | 64 | - Update README.md with a link to the wiki 65 | 66 | ## [1.0.0] - 2017-05-03 67 | 68 | ### Added 69 | 70 | - Initial files 71 | - API: `connect`, `getState`, and `setState` 72 | 73 | [unreleased]: https://github.com/Spreetail/knockout-store/compare/v4.0.0...HEAD 74 | [4.0.0]: https://github.com/Spreetail/knockout-store/compare/v3.0.2...v4.0.0 75 | [3.0.2]: https://github.com/Spreetail/knockout-store/compare/v3.0.1...v3.0.2 76 | [3.0.1]: https://github.com/Spreetail/knockout-store/compare/v3.0.0...v3.0.1 77 | [3.0.0]: https://github.com/Spreetail/knockout-store/compare/v2.0.0...v3.0.0 78 | [2.0.0]: https://github.com/Spreetail/knockout-store/compare/v1.0.2...v2.0.0 79 | [1.0.2]: https://github.com/Spreetail/knockout-store/compare/v1.0.1...v1.0.2 80 | [1.0.1]: https://github.com/Spreetail/knockout-store/compare/v1.0.0...v1.0.1 81 | [1.0.0]: https://github.com/Spreetail/knockout-store/releases/tag/v1.0.0 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Spreetail 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 | # knockout-store 2 | State management for [Knockout](http://knockoutjs.com/) apps. 3 | Inspired by [Redux](http://redux.js.org/) 4 | and [react-redux](https://github.com/reactjs/react-redux). 5 | 6 | Managing app state is hard. While tools like Redux exist to solve this problem, 7 | mixing Redux and Knockout might be overkill for your app. 8 | Knockout already has [observables](http://knockoutjs.com/documentation/observables.html) 9 | which offer some of the functionality provided by a [Redux store](http://redux.js.org/docs/api/Store.html), 10 | namely subscriptions. 11 | 12 | **knockout-store** is a tiny library offering an API for app state management in Knockout apps. 13 | Define your app state once using `setState`, 14 | and then connect your view models with the `connect` method. 15 | This enables developers to decouple view models from one another, 16 | by giving each view model access to the app state instead. 17 | 18 | For a deeper understanding of the library and the motivation behind it, 19 | see [the wiki](https://github.com/Spreetail/knockout-store/wiki). 20 | 21 | ## Installation 22 | The best way to use **knockout-store** is to add it as an npm dependency. 23 | ``` 24 | npm install --save knockout-store 25 | ``` 26 | 27 | Once installed, **knockout-store** supports several types of imports. 28 | 29 | ### ES6 30 | ```javascript 31 | import { connect, getState, setState } from 'knockout-store'; 32 | ``` 33 | 34 | ### UMD Require 35 | ```javascript 36 | const knockoutStore = require('knockout-store'); 37 | ``` 38 | 39 | ### UMD Script Tag 40 | Referencing a script in the `dist` directory on a page will add the [API](#api) methods to `ko.store`. 41 | ```html 42 | 43 | 44 | 45 | ``` 46 | 47 | Then, in JavaScript: 48 | ```javascript 49 | ko.store.setState(someStateObject); 50 | ``` 51 | 52 | ## Usage 53 | Here's a small example, skip to the [API](#api) section for details on the methods. 54 | 55 | ### Setting the App State 56 | ```javascript 57 | import ko from 'knockout'; 58 | import { setState } from 'knockout-store'; 59 | 60 | const state = { 61 | cats: ko.observableArray(['Mr. Whiskers', 'Charles', 'Missy']), 62 | selectedCat: ko.observable() 63 | }; 64 | 65 | setState(state); 66 | ``` 67 | 68 | ### Connecting a View Model 69 | This might look familiar if you've used react-redux. 70 | ```javascript 71 | import { connect } from 'knockout-store'; 72 | 73 | function CatSelectorViewModel(params) { 74 | const self = this; 75 | self.cats = params.cats; // from the state object, see mapStateToParams below 76 | self.selectCat = function(cat) { 77 | params.selectedCat(cat); // also from the state object 78 | } 79 | } 80 | 81 | function mapStateToParams({ cats, selectedCat }) { // the state object 82 | return { cats, selectedCat }; // properties on state to add to view model's params 83 | } 84 | 85 | export default connect(mapStateToParams)(CatSelectorViewModel); 86 | ``` 87 | 88 | ### Connecting Another View Model 89 | ```javascript 90 | import { connect } from 'knockout-store'; 91 | 92 | function SelectedCatDisplayViewModel(params) { 93 | const self = this; 94 | // Since params.selectedCat is an observable, 95 | // this computed will update appropriately 96 | // after selectedCat is updated in the other view model. 97 | self.selectedCatText = ko.computed(() => `You've selected ${params.selectedCat()}!`)); 98 | } 99 | 100 | function mapStateToParams({ selectedCat }) { 101 | return { selectedCat }; 102 | } 103 | 104 | export default connect(mapStateToParams)(SelectedCatDisplayViewModel); 105 | ``` 106 | > Confused? Have a look at [the wiki](https://github.com/Spreetail/knockout-store/wiki) 107 | for a more in-depth example. 108 | 109 | ### Using the Connected View Models 110 | `connect` returns a wrapped view model and can be used like any other view model. 111 | ```javascript 112 | import CatSelectorViewModel from './cat-selector-view-model'; 113 | import SelectedCatDisplayViewModel from './selected-cat-display-view-model'; 114 | 115 | ko.applyBindings(CatSelectorViewModel, document.getElementById('cat-selector')); 116 | ko.applyBindings(SelectedCatDisplayViewModel, document.getElementById('selected-cat-display')); 117 | ``` 118 | 119 | > Note: Use with [Knockout Components](http://knockoutjs.com/documentation/component-overview.html) 120 | for a more modern development experience. 121 | See [knockout-store-todo](https://github.com/Spreetail/knockout-store-todo). 122 | 123 | ## API 124 | ### `setState(state)` 125 | Sets the app state to have value of `state`. 126 | `state` is stored in an observable, which you can access through the `getState()` method (see below). 127 | For most cases, `state` will be an object made up of other observable properties. 128 | In this situation, calling `setState` again will overwrite the object and all subscriptions will be lost. 129 | For this reason, it's unlikely this should be called more than once. 130 | 131 | #### Arguments 132 | - [`state`] (_Object_): The object to store in the app state observable. 133 | 134 | ### `getState()` 135 | Returns the app state observable. 136 | If you need to subscribe directly to the app state, you can do so with this method. 137 | ```javascript 138 | const stateObservable = getState(); 139 | stateObservable.subscribe((newState) => { 140 | // do something with the new state 141 | }); 142 | ``` 143 | 144 | It's usually preferable to connect your view models to the state through the `connect()` method instead (see below). 145 | 146 | ### `connect([mapStateToParams], [mergeParams])` 147 | Connects a view model to the app state. 148 | Pass the view model to be connected to the result of this function. 149 | 150 | #### Arguments 151 | - [`mapStateToParams(state, [ownParams]): stateParams`] (_Function_): 152 | If specified, this argument is a function to map from the app state (`state`) to the `stateParams` object passed to `mergeParams` (see below). `state` will be the value of the observable returned by `getState()` (see above). 153 | If this argument is `null` or not specified, a function returning an empty object is used instead. 154 | - [`mergeParams(stateParams, ownParams): params`] (_Function_): 155 | If specified, this argument is a function responsible for merging `stateParams` (the result of `mapStateToParams`, see above) and `ownParams` (the `params` object the connected view model was called with). 156 | If this argument is `null` or not specified, `Object.assign({}, ownParams, stateParams)` is used instead. 157 | 158 | ## Testing 159 | Run `npm run test` to start the [Karma](https://karma-runner.github.io/1.0/index.html) 160 | test runner with [PhantomJS](http://phantomjs.org/). 161 | You can also run `npm run test-with-chrome` to test with Google Chrome instead of PhantomJS if that's more your thing. 162 | If you just want to run the tests once, you can use `npm run test-once`. 163 | 164 | ## License 165 | Licensed under the [MIT License](https://opensource.org/licenses/MIT). 166 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Observable, components } from 'knockout'; 2 | 3 | export function setState(state: T): void; 4 | 5 | export function getState(): Observable; 6 | 7 | interface ViewModelFactoryFunction { 8 | (params?: components.ViewModelParams): components.ViewModel; 9 | } 10 | 11 | interface ViewModelInstantiator 12 | extends components.ViewModelConstructor, 13 | ViewModelFactoryFunction {} 14 | 15 | interface MapStateToParamsFn { 16 | ( 17 | state?: T, 18 | ownParams?: components.ViewModelParams 19 | ): components.ViewModelParams; 20 | } 21 | 22 | interface MergeParamsFn { 23 | ( 24 | stateParams: components.ViewModelParams, 25 | ownParams: components.ViewModelParams 26 | ): components.ViewModelParams; 27 | } 28 | 29 | export function connect( 30 | mapStateToParams?: MapStateToParamsFn | null, 31 | mergeParams?: MergeParamsFn | null 32 | ): ( 33 | viewModel: components.ViewModelConstructor | ViewModelFactoryFunction 34 | ) => ViewModelInstantiator; 35 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | 3 | // Use Chromium executable from puppeteer to test 4 | process.env.CHROME_BIN = puppeteer.executablePath(); 5 | 6 | // Karma configuration 7 | // Generated on Tue May 02 2017 14:29:01 GMT-0500 (Central Daylight Time) 8 | 9 | module.exports = function(config) { 10 | config.set({ 11 | // base path that will be used to resolve all patterns (eg. files, exclude) 12 | basePath: '', 13 | 14 | // frameworks to use 15 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 16 | frameworks: ['mocha', 'chai'], 17 | 18 | // list of files / patterns to load in the browser 19 | files: [{ pattern: 'tests/**/*.spec.js', watched: false }], 20 | 21 | // list of files to exclude 22 | exclude: [], 23 | 24 | // preprocess matching files before serving them to the browser 25 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 26 | preprocessors: { 27 | 'tests/**/*.spec.js': ['rollup'], 28 | }, 29 | 30 | rollupPreprocessor: { 31 | plugins: [ 32 | require('rollup-plugin-node-resolve')(), 33 | require('rollup-plugin-commonjs')(), 34 | require('rollup-plugin-babel')(), 35 | ], 36 | format: 'es', 37 | sourcemap: 'inline', 38 | }, 39 | 40 | // test results reporter to use 41 | // possible values: 'dots', 'progress' 42 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 43 | reporters: ['progress'], 44 | 45 | // web server port 46 | port: 9876, 47 | 48 | // enable / disable colors in the output (reporters and logs) 49 | colors: true, 50 | 51 | // level of logging 52 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 53 | logLevel: config.LOG_INFO, 54 | 55 | // enable / disable watching file and executing tests whenever any file changes 56 | autoWatch: true, 57 | 58 | // start these browsers 59 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 60 | browsers: ['ChromeHeadless'], 61 | 62 | // Continuous Integration mode 63 | // if true, Karma captures browsers, runs the tests and exits 64 | singleRun: false, 65 | 66 | // Concurrency level 67 | // how many browser should be started simultaneous 68 | concurrency: Infinity, 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knockout-store", 3 | "version": "3.0.2", 4 | "description": "State management for Knockout apps.", 5 | "main": "dist/knockout-store.js", 6 | "module": "dist/knockout-store.es.js", 7 | "types": "index.d.ts", 8 | "dependencies": {}, 9 | "devDependencies": { 10 | "babel-core": "^6.26.0", 11 | "babel-plugin-transform-object-assign": "^6.22.0", 12 | "babel-preset-es2015-rollup": "^3.0.0", 13 | "chai": "^4.1.2", 14 | "eslint": "^4.12.1", 15 | "eslint-config-spreetail": "^3.0.0", 16 | "karma": "^3.0.0", 17 | "karma-chai": "^0.1.0", 18 | "karma-chrome-launcher": "^2.2.0", 19 | "karma-mocha": "^1.3.0", 20 | "karma-rollup-preprocessor": "^5.0.2", 21 | "knockout": "^3.5.1", 22 | "mocha": "^4.0.1", 23 | "puppeteer": "^3.0.2", 24 | "rimraf": "^2.6.2", 25 | "rollup": "^0.52.1", 26 | "rollup-plugin-babel": "^3.0.2", 27 | "rollup-plugin-commonjs": "^8.2.6", 28 | "rollup-plugin-node-resolve": "^3.0.0", 29 | "rollup-plugin-uglify": "^2.0.1" 30 | }, 31 | "peerDependencies": { 32 | "knockout": "^3.5.0" 33 | }, 34 | "scripts": { 35 | "test": "karma start", 36 | "test-once": "karma start --single-run", 37 | "test-with-chrome": "karma start --browsers Chrome", 38 | "lint": "eslint .", 39 | "build:es": "rollup --config rollup/rollup.es.config.js", 40 | "build:umd": "rollup --config rollup/rollup.umd.config.js", 41 | "build:umd:min": "rollup --config rollup/rollup.umd.min.config.js", 42 | "build": "npm run build:es && npm run build:umd && npm run build:umd:min", 43 | "clean": "rimraf dist", 44 | "build-fresh": "npm run clean && npm run build", 45 | "preversion": "npm run lint && npm run test-once", 46 | "prepublishOnly": "npm run build-fresh", 47 | "prettier": "prettier --write \"{{src,tests,rollup}/**/*.js,index.d.ts}\"" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "git+https://github.com/Spreetail/knockout-store.git" 52 | }, 53 | "files": [ 54 | "dist", 55 | "index.d.ts" 56 | ], 57 | "keywords": [ 58 | "knockout", 59 | "state", 60 | "store", 61 | "connect" 62 | ], 63 | "author": "Alex Bainter (https://alexbainter.com)", 64 | "license": "MIT", 65 | "bugs": { 66 | "url": "https://github.com/Spreetail/knockout-store/issues" 67 | }, 68 | "homepage": "https://github.com/Spreetail/knockout-store#readme", 69 | "publishConfig": { 70 | "registry": "https://registry.npmjs.org/" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /rollup/rollup.base.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | 3 | const baseConfig = { 4 | input: 'src/index.js', 5 | external: ['knockout'], 6 | globals: { 7 | knockout: 'ko', 8 | }, 9 | plugins: [ 10 | babel({ 11 | exclude: 'node_modules/**', 12 | }), 13 | ], 14 | }; 15 | 16 | export default baseConfig; 17 | -------------------------------------------------------------------------------- /rollup/rollup.es.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from './rollup.base.config'; 2 | const { module } = require('../package.json'); 3 | 4 | const esPartialConfig = { 5 | output: { 6 | format: 'es', 7 | file: module, 8 | }, 9 | }; 10 | 11 | const esConfig = Object.assign({}, baseConfig, esPartialConfig); 12 | 13 | export default esConfig; 14 | -------------------------------------------------------------------------------- /rollup/rollup.umd.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from './rollup.base.config.js'; 2 | 3 | const { main } = require('../package.json'); 4 | 5 | const umdPartialConfig = { 6 | output: { 7 | format: 'umd', 8 | file: main, 9 | name: 'ko.store', 10 | }, 11 | }; 12 | 13 | const umdConfig = Object.assign({}, baseConfig, umdPartialConfig); 14 | 15 | export default umdConfig; 16 | -------------------------------------------------------------------------------- /rollup/rollup.umd.min.config.js: -------------------------------------------------------------------------------- 1 | import uglify from 'rollup-plugin-uglify'; 2 | import umdConfig from './rollup.umd.config'; 3 | 4 | umdConfig.plugins = umdConfig.plugins || []; 5 | umdConfig.plugins.push(uglify()); 6 | umdConfig.output.file = umdConfig.output.file.replace(/\.js$/, '.min.js'); 7 | 8 | export default umdConfig; 9 | -------------------------------------------------------------------------------- /src/connect.js: -------------------------------------------------------------------------------- 1 | import { getState } from './store'; 2 | 3 | const defaultMapStateToParams = () => ({}); 4 | const defaultMergeParams = (stateParams, ownParams) => 5 | Object.assign({}, ownParams, stateParams); 6 | 7 | const throwIfNotAFunction = (o, message) => { 8 | if (typeof o !== 'function') { 9 | throw new Error(message); 10 | } 11 | }; 12 | 13 | const makeNullableFunctionArgInvalidTypeMessage = (arg, argName) => 14 | `Invalid type '${typeof arg}' for connect parameter ${argName}. ${argName} must be a null or a function.`; 15 | 16 | const throwIfNullableFuctionArgNotAFunction = (arg, argName) => { 17 | throwIfNotAFunction( 18 | arg, 19 | makeNullableFunctionArgInvalidTypeMessage(arg, argName) 20 | ); 21 | }; 22 | 23 | const connect = ( 24 | mapStateToParams = defaultMapStateToParams, 25 | mergeParams = defaultMergeParams 26 | ) => { 27 | const mapStateToParamsFunc = 28 | mapStateToParams === null ? defaultMapStateToParams : mapStateToParams; 29 | const mergeParamsFunc = 30 | mergeParams === null ? defaultMergeParams : mergeParams; 31 | 32 | throwIfNullableFuctionArgNotAFunction( 33 | mapStateToParamsFunc, 34 | 'mapStateToParams' 35 | ); 36 | throwIfNullableFuctionArgNotAFunction(mergeParamsFunc, 'mergeParams'); 37 | 38 | return (ViewModel) => { 39 | throwIfNotAFunction( 40 | ViewModel, 41 | `Invalid type '${typeof ViewModel}' for ViewModel passed to result of connect(). ViewModel must be a function.` 42 | ); 43 | return (ownParams) => { 44 | const state = getState(); 45 | const stateParams = mapStateToParamsFunc(state(), ownParams); 46 | const mergedParams = mergeParamsFunc(stateParams, ownParams); 47 | return new ViewModel(mergedParams); 48 | }; 49 | }; 50 | }; 51 | 52 | export default connect; 53 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import connect from './connect'; 2 | import { setState, getState } from './store'; 3 | 4 | export { setState, getState, connect }; 5 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout'; 2 | 3 | const stateObservable = ko.observable(); 4 | 5 | function setState(state) { 6 | stateObservable(state); 7 | } 8 | 9 | function getState() { 10 | return stateObservable; 11 | } 12 | 13 | export { setState, getState }; 14 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/connect.spec.js: -------------------------------------------------------------------------------- 1 | import { connect } from '../src/index'; 2 | import { setState } from '../src/store'; 3 | 4 | function viewModelMock(params) { 5 | return { params }; 6 | } 7 | 8 | describe('connect', () => { 9 | const testState = { 10 | prop1: 1, 11 | prop2: 2, 12 | }; 13 | beforeEach(() => { 14 | setState(testState); 15 | }); 16 | afterEach(() => { 17 | setState(undefined); // eslint-disable-line no-undefined 18 | }); 19 | 20 | it('should return a function', () => { 21 | expect(connect()).to.be.a('function'); 22 | }); 23 | it('should return a function when return function is called', () => { 24 | expect(connect()(viewModelMock)).to.be.a('function'); 25 | }); 26 | it('should throw an Error if returned function is called without a viewmodel', () => { 27 | expect(() => connect()()).to.throw(Error); 28 | expect(() => connect()('not a viewmodel')).to.throw(Error); 29 | }); 30 | it('should throw an Error if called with mapStateToParams not null or a function', () => { 31 | expect(() => connect()('not null or a function')).to.throw(Error); 32 | }); 33 | it('should throw an Error if called with mergeParams not null or a function', () => { 34 | expect(() => connect()(null, 'not null or a function')).to.throw(Error); 35 | }); 36 | it('should use the default mapStateToParams if given null', () => { 37 | const mergeParams = (stateParams) => { 38 | expect(stateParams).to.eql({}); 39 | mergeParams.called = true; 40 | }; 41 | expect(connect(null, mergeParams)(viewModelMock)()); 42 | expect(mergeParams).to.have.property('called', true); 43 | }); 44 | it('should use the default mergeParams if given null', () => { 45 | const params = { prop2: 'something', prop3: 'something else' }; 46 | const expectedMergedParams = Object.assign({}, {}, params); 47 | const instantiatedVM = connect(null, null)(viewModelMock)(params); 48 | expect(instantiatedVM.params).to.eql(expectedMergedParams); 49 | }); 50 | 51 | describe('mapStateToParams', () => { 52 | it('should map state to params', () => { 53 | function mapStateToParams({ prop1, prop2 }) { 54 | return { prop1, prop2 }; 55 | } 56 | const ConnectedViewModel = connect(mapStateToParams)(viewModelMock); 57 | const { params } = new ConnectedViewModel({}); 58 | expect(params).to.have.property('prop1', 1); 59 | expect(params).to.have.property('prop2', 2); 60 | }); 61 | it('should only map desired state properties', () => { 62 | function mapStateToParams({ prop1 }) { 63 | return { prop1 }; 64 | } 65 | const ConnectedViewModel = connect(mapStateToParams)(viewModelMock); 66 | const { params } = new ConnectedViewModel({}); 67 | expect(params).to.have.property('prop1', 1); 68 | expect(params).to.not.have.property('prop2'); 69 | }); 70 | it("should not interfere with a viewModel's ownParams", () => { 71 | const ConnectedViewModel = connect()(viewModelMock); 72 | const { params } = new ConnectedViewModel({ ownParam1: 1, ownParam2: 2 }); 73 | expect(params).to.have.property('ownParam1', 1); 74 | expect(params).to.have.property('ownParam2', 2); 75 | }); 76 | it('should execute mapping when instantiating the view model', () => { 77 | let mapStateToParamsCalled = false; 78 | function mapStateToParams() { 79 | mapStateToParamsCalled = true; 80 | } 81 | const ConnectedViewModel = connect(mapStateToParams)(viewModelMock); 82 | expect(mapStateToParamsCalled).to.be.false; 83 | new ConnectedViewModel(); // eslint-disable-line no-new 84 | expect(mapStateToParamsCalled).to.be.true; 85 | }); 86 | it('should pass state followed by ownParams to mapStateToParams', () => { 87 | const params = { 88 | ownParam1: 1, 89 | }; 90 | function mapStateToParams(state, ownParams) { 91 | expect(state).to.equal(testState); 92 | expect(ownParams).to.equal(params); 93 | } 94 | const ConnectedViewModel = connect(mapStateToParams)(viewModelMock); 95 | new ConnectedViewModel(params); // eslint-disable-line no-new 96 | }); 97 | }); 98 | 99 | describe('mergeParams', () => { 100 | it('should resolve param collisions with stateParams by default', () => { 101 | function mapStateToParams({ prop1, prop2 }) { 102 | return { prop1, prop2 }; 103 | } 104 | const ownParams = { 105 | prop1: 'overwritten', 106 | prop2: 'overwritten', 107 | prop3: 3, 108 | }; 109 | const ConnectedViewModel = connect(mapStateToParams)(viewModelMock); 110 | const { params } = new ConnectedViewModel(ownParams); 111 | expect(params).to.have.property('prop1', 1); 112 | expect(params).to.have.property('prop2', 2); 113 | expect(params).to.have.property('prop3', 3); 114 | }); 115 | it('should pass stateParams followed by ownParams to mergeParams', () => { 116 | const params = { 117 | ownParam1: 1, 118 | }; 119 | function mapStateToParams(state) { 120 | return state; 121 | } 122 | let mergeParamsCalled = false; 123 | function mergeParams(stateParams, ownParams) { 124 | expect(stateParams).to.equal(testState); 125 | expect(ownParams).to.equal(params); 126 | mergeParamsCalled = true; 127 | } 128 | const ConnectedViewModel = connect( 129 | mapStateToParams, 130 | mergeParams 131 | )(viewModelMock); 132 | new ConnectedViewModel(params); // eslint-disable-line no-new 133 | expect(mergeParamsCalled).to.be.true; 134 | }); 135 | it('should use mergeParams to resolve collisions', () => { 136 | const testParams = { 137 | ownParam1: 1, 138 | }; 139 | function mapStateToParams(state) { 140 | return state; 141 | } 142 | function mergeParams(stateParams, ownParams) { 143 | return ownParams; 144 | } 145 | const ConnectedViewModel = connect( 146 | mapStateToParams, 147 | mergeParams 148 | )(viewModelMock); 149 | const { params } = new ConnectedViewModel(testParams); 150 | expect(params).to.equal(testParams); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /tests/store.spec.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout'; 2 | import { getState, setState } from '../src/store'; 3 | 4 | describe('store', () => { 5 | afterEach(() => { 6 | setState(undefined); // eslint-disable-line no-undefined 7 | }); 8 | describe('getState', () => { 9 | it('should return an observable', () => { 10 | const state = getState(); 11 | expect(ko.isObservable(state)).to.be.true; 12 | }); 13 | it('should return an observable with an undefined value', () => { 14 | const state = getState(); 15 | expect(state()).to.be.undefined; 16 | }); 17 | }); 18 | it('should update state', () => { 19 | const initialState = { 20 | prop1: 1, 21 | prop2: { 22 | prop3: 2, 23 | }, 24 | }; 25 | setState(initialState); 26 | const state = getState(); 27 | // checks for '==='' equivalence 28 | expect(state()).to.equal(initialState); 29 | }); 30 | }); 31 | --------------------------------------------------------------------------------