├── .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 |
--------------------------------------------------------------------------------