├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE.md
├── README.md
├── index.js
├── internals
└── utils.js
├── lib
├── ImmutableStore.js
├── createImmutableContainer.js
├── createImmutableMixin.js
├── createImmutableStore.js
└── index.js
├── mixins
├── ImmutableMixin.js
└── index.js
├── package.json
└── tests
├── lib
├── ImmutableStore.js
├── createImmutableContainer.js
├── createImmutableMixin.js
└── createImmutableStore.js
└── mixins
└── ImmutableMixin.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | example.js
2 | build/**
3 | artifacts/**
4 | lib/ImmutableStore.js
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "browser": true
5 | },
6 | "rules": {
7 | "array-bracket-spacing": [2, "never"],
8 | "computed-property-spacing": [2, "never"],
9 | "consistent-this": [2, "self"],
10 | "guard-for-in": 2,
11 | "handle-callback-err": 2,
12 | "indent" : 2,
13 | "keyword-spacing": 2,
14 | "new-cap": 0,
15 | "no-catch-shadow": 2,
16 | "no-else-return": 2,
17 | "no-floating-decimal": 2,
18 | "no-inline-comments": 1,
19 | "no-lonely-if": 2,
20 | "no-multiple-empty-lines": 2,
21 | "no-nested-ternary": 2,
22 | "no-self-compare": 2,
23 | "no-sync": 0,
24 | "no-throw-literal": 2,
25 | "no-underscore-dangle" : 0,
26 | "no-unused-expressions" : 1,
27 | "no-unused-vars" : [ 2, { "args" : "none" } ],
28 | "no-void": 2,
29 | "no-warning-comments": 1,
30 | "object-curly-spacing": [2, "never"],
31 | "padded-blocks" : [ 2, "never"],
32 | "quotes" : [ 2,"single"],
33 | "radix": 2,
34 | "space-before-function-paren" : [ 2, {"anonymous": "always", "named": "never"}],
35 | "valid-jsdoc": [2],
36 | "vars-on-top": 0,
37 | "wrap-iife" : 2,
38 | "wrap-regex": 2
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | *.tgz
3 | .nyc_output
4 | artifacts
5 | node_modules
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *.log
2 | *.tgz
3 | .eslintignore
4 | .eslintrc
5 | .npmignore
6 | .nyc_output
7 | .travis.yml
8 | artifacts
9 | tests
10 | artifacts
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - "8"
5 | - "6"
6 | - "4"
7 | after_success:
8 | - "cat artifacts/lcov.info | ./node_modules/coveralls/bin/coveralls.js"
9 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, Yahoo Inc. All rights reserved.
2 |
3 | Redistribution and use of this software in source and binary forms,
4 | with or without modification, are permitted provided that the following
5 | conditions are met:
6 |
7 | * Redistributions of source code must retain the above
8 | copyright notice, this list of conditions and the
9 | following disclaimer.
10 |
11 | * Redistributions in binary form must reproduce the above
12 | copyright notice, this list of conditions and the
13 | following disclaimer in the documentation and/or other
14 | materials provided with the distribution.
15 |
16 | * Neither the name of Yahoo Inc. nor the names of its
17 | contributors may be used to endorse or promote products
18 | derived from this software without specific prior
19 | written permission of Yahoo Inc.
20 |
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
22 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
23 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
24 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # fluxible-immutable-utils
2 |
3 | [](http://badge.fury.io/js/fluxible-immutable-utils)
4 | [](https://travis-ci.org/yahoo/fluxible-immutable-utils)
5 | [](https://david-dm.org/yahoo/fluxible-immutable-utils)
6 | [](https://david-dm.org/yahoo/fluxible-immutable-utils#info=devDependencies)
7 | [](https://coveralls.io/r/yahoo/fluxible-immutable-utils)
8 |
9 | This package provides easy to use mixins/utils for both fluxible stores and react components.
10 |
11 | ```bash
12 | $ npm install --save fluxible-immutable-utils
13 | ```
14 |
15 | ## `createImmutableContainer`
16 |
17 | This method creates an immutable higher order component.
18 |
19 | ```js
20 |
21 | var MyComponent = createReactClass({
22 | displayName: 'MyComponent',
23 |
24 | ...
25 | });
26 |
27 | var createImmutableContainer = require('fluxible-immutable-utils').createImmutableContainer;
28 |
29 | // Wraps your component in an immutable container.
30 | // Prevents renders when props are the same
31 | module.exports = createImmutableContainer(MyComponent);
32 |
33 | // Wraps your component in an immutable container that listens to stores
34 | // and pass its state down as props
35 | module.exports = createImmutableContainer(MyComponent, {
36 | stores: [SomeStore],
37 | getStateFromStores: {
38 | SomeStore: function (store) {
39 | return {
40 | someState: store.state;
41 | }
42 | }
43 | }
44 | });
45 | ```
46 |
47 | ## `ComponentMixin`
48 | A mixin that provides convenience methods for using Immutable.js inside of react components. Note that this mixin uses the initializeComponent method for setup, and any components that use this mixin should define a 'getStateOnChange' function for generating component state (see below).
49 |
50 | This mixin has several purposes:
51 | - Checks that the objects in state/props of each component are an Immutable Map.
52 | - Implements a default shouldComponentUpdate method.
53 | - Provides a convenience method for dealing with state changes/component
54 | initialization.
55 |
56 | ### Immutalizing State
57 | The mixin uses the initalizeState method to set up all default functions, and checks for a method named 'getStateOnChange' in order to get the initial state object. If used with fluxible's FluxibleMixin, getStateOnChange will also be called whenever a store is updated (if onChange is not defined). This allows a reduction in boilerplate by not having to define separate functions for app initialization/store updates (since components should handle state the same in either case).
58 |
59 | The mixin expects props/state to remain immutable throughout a component's lifecycle and only shallowly examines the props object when checking for data equality. Thus it is HIGHLY recommended to pass in immutable objects as props/state to a component using this mixin (the mixin will warn when not doing so). You may configure which objects to check by setting the ignoreImmutableObjects static property (example below).
60 |
61 | ### shouldComponentUpdate
62 | The immutable mixin implements a version of shouldComponentUpdate to prevent needless re-rendering of components when the props/state haven't changed (by checking if the new props/state have been changed from the old props/state). If a component provides its own shouldComponentUpdate method, then the default implementation will not be used.
63 |
64 | ### getStateOnChange
65 | Since ImmutableMixin must use the initializeComponent method for setting up default methods, it cannot be used by the components. Instead, ImmutableMixin will call the 'getStateOnChange' method in getInitialState. This method will also be called if used with the FluxibleMixin on store changes (again, only if onChange is not defined) which helps to reduce boilerplate within components.
66 |
67 | ### API
68 |
69 | #### shouldUpdate (nextProps, nextState)
70 |
71 | Utility method that is set as the `shouldComponentUpdate` method in a component unless it is already defined. Checks whether the props/state of the component has changed so that we know whether to render or not.
72 |
73 | 1. {Object} The next props object
74 | 2. {Object} The next state object
75 |
76 | #### defaultOnChange ()
77 |
78 | Utility method that is set as the `onChange` method. A default onChange function that sets the the components state from the getStateOnChange method. This is only set if a component does not implement its own onChange function. Typically used with the fluxibleMixin.
79 |
80 | #### getInitialState ()
81 |
82 | Called internally by React. Sets up a few of the immutable methods and then returns the state of the component, after ensuring it is immutable. If getStateOnChange() is not defined, then just sets the state to null.
83 |
84 | **Example**
85 |
86 | ```jsx
87 | // MyReactComponent.jsx
88 |
89 | var ImmutableMixin = require('fluxible-immutable-utils').ComponentMixin;
90 |
91 | module.exports = createReactClass({
92 | displayName: 'MyReactComponent',
93 | mixins: [ImmutableMixin],
94 | getStateOnChange: function () {
95 | if (!this.state) {
96 | //initialize here if needed
97 | }
98 | return {
99 | someStateProperty: 'foo'
100 | };
101 | },
102 |
103 | render: function () {
104 | return My React Component;
105 | }
106 | });
107 |
108 | var myObject = {
109 | foo: 'bar'
110 | };
111 |
115 | ```
116 |
117 | #### Configuring the Mixin
118 | If you are using third party libraries/have a special case where you don't want the mixin to consider some of the keys of props/state, you have two options. First, you can set the ignoreImmutableCheck object to skip the check for immutability for any keys you decide. Second, if you want the mixin to also ignore a key when checking for data equality in props/state, you can set the key value to the flag `SKIP_SHOULD_UPDATE`. You must set these values inside a component's `statics` field (or in a config, see below), and they must be set seperately for props/state. You can also turn off all warnings by settings the ignoreAllWarnings flag.
119 |
120 | **Example**
121 |
122 | ```jsx
123 | // MyReactComponent.jsx
124 |
125 | var ImmutableMixin = require('fluxible-immutable-utils').ComponentMixin;
126 |
127 | module.exports = createReactClass({
128 | displayName: 'MyReactComponent',
129 | mixins: [ImmutableMixin],
130 | statics: {
131 | ignoreAllWarnings: (process.env.NODE_ENV !== 'dev') // turn off all warnings when not in dev mode
132 | ignoreImmutableCheck: {
133 | props: {
134 | someKey: true // don't check someKey for immutablility in props
135 | },
136 | state: {
137 | anotherKey: 'SKIP_SHOULD_UPDATE' // don't check anotherKey for immutablility in props, AND don't check its value is shouldComponentUpdate
138 | }
139 |
140 | }
141 | },
142 |
143 | ...rest of component follows...
144 | });
145 | ```
146 |
147 | If you want to just pass around a common config, then use:
148 | ```jsx
149 | var ImmutableMixin = require('fluxible-immutable-utils').createComponentMixin(myConfig);
150 | ```
151 | Where myConfig has the same structure as the statics above.
152 |
153 | ## `ImmutableStore`
154 |
155 | A class to inherit similar to the fluxible addon `BaseStore`. Internally it inherits [`'fluxible/addons/BaseStore`](https://github.com/yahoo/fluxible/blob/master/docs/api/addons/BaseStore.md).
156 |
157 | The main use case for this method is to reduce boilerplate when implementing immutable [`fluxible`](fluxible.io) stores.
158 |
159 | The helper adds a new property and some helper methods to the created store
160 | * `_state` {[Map](http://facebook.github.io/immutable-js/docs/#/Map)} - The root `Immutable` where all data in the store will be saved.
161 |
162 | * `setState(newState, [event], [payload])` {Function} - This method replaces `this._state` with `newState` (unless they were the same) and then calls `this.emit(event, payload)`.
163 | * If `event` is *falsy* it will call `this.emitChange(payload)`
164 | * The method also ensures that `_state` remains immutable by auto-converting `newState` to an immutable object.
165 |
166 | * `mergeState(newState, [event], [payload])` {Function} - This method does a shallow merge with `this._state` and then calls `this.emit(event, payload)`.
167 | * If `event` is *falsy* it will call `this.emitChange(payload)`
168 | * The method also ensures that `_state` remains immutable by auto-converting `newState` to an immutable object.
169 |
170 | * `getState()` {Function} - This method returns the `this._state`.
171 |
172 | * `get(key)` {Function} - Get a value by key from the store.
173 |
174 | and creates defaults for the following [fluxible store](http://fluxible.io/api/stores.html) methods
175 | * [`constructor()`](http://fluxible.io/api/stores.html#constructor) - The default implementations creates a `_state` property on the store and initializes it to [`Immutable.Map`](http://facebook.github.io/immutable-js/docs/#/Map)
176 |
177 | * [`rehydrate(state)`](http://fluxible.io/api/stores.html#rehydrate-state-) - The default implementation hydrates `_state`
178 |
179 | * [`dehydrate()`](http://fluxible.io/api/stores.html#dehydrate-) - The default implementation simply returns `_state` which is `Immutable` (due to all `Immutable` objects implementing a [`toJSON`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON_behavior) function, `_state` can be directly passed to `JSON.stringify`)
180 |
181 | **Note** that all defaults can still be overwritten when creating the store.
182 |
183 | **Note 2** Avoid returning a `_state.toJS()` from a store when using it with the createImmutableContainer since an ImmutableContainer expects and uses the Immutable data to do comparisons when deciding to re-render.
184 |
185 | ### Example Usage
186 |
187 | ```js
188 | // FooStore.js
189 |
190 | import {ImmutableStore} from 'fluxible-immutable-utils';
191 |
192 | class FooStore extends ImmutableStore {
193 | // public accessors
194 | getBar: function (id) {
195 | return this._state.get('bar');
196 | }
197 |
198 | // private mutators, these should only be called via dispatch
199 | _onNewFoo(data) {
200 | // data = { foo: 'Hello', bar: 'World' }
201 | this.setState(data);
202 | }
203 |
204 | _onNewBar(bar) {
205 | // This will just update bar and leave foo with the same state
206 | this.mergeState({ bar: bar });
207 | }
208 | }
209 |
210 | FooStore.storeName = 'FooStore';
211 |
212 | FooStore.handlers = {
213 | NEW_FOO: '_onNewFooBar',
214 | NEW_FOOS: '_onNewBar'
215 | };
216 |
217 | export default FooStore;
218 | ```
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ComponentMixin: require('./mixins/ImmutableMixin'),
3 | createImmutableMixin: require('./lib/createImmutableMixin'),
4 | createImmutableContainer: require('./lib/createImmutableContainer'),
5 | createImmutableStore: require('./lib/createImmutableStore'),
6 | ImmutableStore: require('./lib/ImmutableStore')
7 | };
8 |
--------------------------------------------------------------------------------
/internals/utils.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, Yahoo Inc. All rights reserved.
3 | * Copyrights licensed under the New BSD License.
4 | * See the accompanying LICENSE file for terms.
5 | */
6 | 'use strict';
7 |
8 | var Immutable = require('immutable');
9 | var isImmutable = Immutable.Iterable.isIterable;
10 | var isReactElement = require('react').isValidElement;
11 |
12 | function isNonImmutable(item) {
13 | return (
14 | item
15 | && typeof item === 'object'
16 | && !isReactElement(item)
17 | && !isImmutable(item)
18 | );
19 | }
20 |
21 | function warnNonImmutable(component, prop) {
22 | console.warn('Component ' +
23 | '"' + component.constructor.displayName + '"' +
24 | ' received non-immutable object for ' +
25 | '"' + prop + '"');
26 | }
27 |
28 | function merge(dest, src) {
29 | if (!dest) {
30 | dest = {};
31 | }
32 |
33 | if (typeof src === 'object') {
34 | Object.keys(src).forEach(function mergeCb(prop) {
35 | dest[prop] = src[prop];
36 | });
37 | }
38 |
39 | return dest;
40 | }
41 |
42 | module.exports = {
43 | merge: merge,
44 | isNonImmutable: isNonImmutable,
45 | warnNonImmutable: warnNonImmutable
46 | };
47 |
--------------------------------------------------------------------------------
/lib/ImmutableStore.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, Yahoo Inc. All rights reserved.
3 | * Copyrights licensed under the New BSD License.
4 | * See the accompanying LICENSE file for terms.
5 | */
6 |
7 | var BaseStore = require('fluxible/addons/BaseStore');
8 | var Immutable = require('immutable');
9 | var inherits = require('inherits');
10 |
11 | function ImmutableStore(dispatcher) {
12 | this._state = Immutable.Map();
13 | BaseStore.call(this, dispatcher);
14 | };
15 |
16 | inherits(ImmutableStore, BaseStore);
17 |
18 | ImmutableStore.prototype.dehydrate = function() {
19 | return this._state;
20 | }
21 |
22 | ImmutableStore.prototype.get = function(key) {
23 | return Array.isArray(key) ? this._state.getIn(key) : this._state.get(key);
24 | }
25 |
26 | ImmutableStore.prototype.getState = function() {
27 | return this.dehydrate();
28 | }
29 |
30 | ImmutableStore.prototype.mergeState = function(stateFragment, event, payload) {
31 | return this.setState(
32 | this._state.merge(stateFragment),
33 | event,
34 | payload
35 | );
36 | }
37 |
38 | ImmutableStore.prototype.rehydrate = function(state) {
39 | this._state = Immutable.fromJS(state);
40 | }
41 |
42 | ImmutableStore.prototype.setState = function(newState, event, payload) {
43 | newState = Immutable.fromJS(newState);
44 |
45 | if (this._state.equals(newState)) {
46 | return false;
47 | }
48 |
49 | this._state = newState;
50 |
51 | if (event){
52 | this.emit(event, payload);
53 | }
54 | else {
55 | this.emitChange(payload);
56 | }
57 |
58 | return true;
59 | }
60 |
61 | module.exports = ImmutableStore;
62 |
--------------------------------------------------------------------------------
/lib/createImmutableContainer.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, Yahoo Inc. All rights reserved.
3 | * Copyrights licensed under the New BSD License.
4 | * See the accompanying LICENSE file for terms.
5 | */
6 |
7 | 'use strict';
8 |
9 | var React = require('react');
10 | var connectToStores = require('fluxible-addons-react/connectToStores');
11 | var createReactClass = require('create-react-class');
12 | var shallowequal = require('shallowequal');
13 | var utils = require('../internals/utils');
14 |
15 | function getIgnoredProps(ignore) {
16 | if (!Array.isArray(ignore)) {
17 | return ignore || {};
18 | }
19 |
20 | var ignoredProps = {};
21 |
22 | ignore.forEach(function (prop) {
23 | ignoredProps[prop] = true;
24 | });
25 |
26 | return ignoredProps;
27 | }
28 |
29 | module.exports = function createImmutableContainer(Component, options) {
30 | options = options || {};
31 | var ignore = options.ignore;
32 | var ignoreWarnings = options.ignoreWarnings;
33 | var getStateFromStores = options.getStateFromStores;
34 | var stores = options.stores || Object.keys(getStateFromStores || {});
35 | var componentName = Component.displayName || Component.name;
36 |
37 | var ImmutableComponent = createReactClass({
38 | displayName: componentName + ':Immutable',
39 |
40 | getDefaultProps: function () {
41 | return {};
42 | },
43 |
44 | checkImmutable: function (prop) {
45 | if (
46 | !this._ignoredProps[prop] &&
47 | utils.isNonImmutable(this.props[prop])
48 | ) {
49 | utils.warnNonImmutable(this, prop);
50 | }
51 | },
52 |
53 | checkAllImmutable: function () {
54 | if (!ignoreWarnings) {
55 | Object.keys(this.props).forEach(this.checkImmutable);
56 | }
57 | },
58 |
59 | componentWillMount: function () {
60 | this._ignoredProps = getIgnoredProps(ignore);
61 | this.checkAllImmutable();
62 | },
63 |
64 | componentWillUpdate: function () {
65 | this.checkAllImmutable();
66 | },
67 |
68 | shouldComponentUpdate: function (nextProps, nextState) {
69 | // Backward compatibility for components with no state
70 | nextState = nextState || null;
71 | return !shallowequal(this.props, nextProps) || !shallowequal(this.state, nextState);
72 | },
73 |
74 | render: function () {
75 | return React.createElement(Component, this.props);
76 | }
77 | });
78 |
79 | return stores.length ?
80 | connectToStores(ImmutableComponent, stores, getStateFromStores) :
81 | ImmutableComponent;
82 | };
83 |
--------------------------------------------------------------------------------
/lib/createImmutableMixin.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, Yahoo Inc. All rights reserved.
3 | * Copyrights licensed under the New BSD License.
4 | * See the accompanying LICENSE file for terms.
5 | */
6 | 'use strict';
7 |
8 | var utils = require('../internals/utils');
9 | var GET_STATE_FUNCTION = 'getStateOnChange';
10 | var IGNORE_IMMUTABLE_CHECK = 'ignoreImmutableCheck';
11 | var IGNORE_EQUALITY_CHECK_FLAG = 'SKIP_SHOULD_UPDATE';
12 |
13 | /**
14 | * Make sure an item is not a non immutable object. The only exceptions are valid
15 | * react elements and objects specifically set to be ignored.
16 | * @param {String} key The item to test's key
17 | * @param {Any} item The item to test
18 | * @param {Object} component The component
19 | * @return {Boolean} True if non-immutable object, else false.
20 | */
21 | function checkNonImmutableObject(key, item, component) {
22 | if (utils.isNonImmutable(item)) {
23 | console.warn('WARN: component: ' + component.constructor.displayName
24 | + ' received non-immutable object: ' + key);
25 | console.trace();
26 | }
27 | }
28 |
29 | /**
30 | * Check that all an objects fields are either primitives or immutable objects.
31 | * @param {Object} object The object to check
32 | * @param {Object} component The component being checked
33 | * @param {Object} ignoreImmutableCheckKeys Objects to skip a check for
34 | * @return {Undefined} none
35 | */
36 | function checkObjectProperties(object, component, ignoreImmutableCheckKeys) {
37 | if (component.ignoreAllWarnings || !object || typeof object !== 'object') {
38 | return;
39 | }
40 | var ignoreImmutableCheck;
41 | Object.keys(object).forEach(function objectIterator(key) {
42 | ignoreImmutableCheck = ignoreImmutableCheckKeys[key];
43 | if (!ignoreImmutableCheck) {
44 | checkNonImmutableObject(key, object[key], component);
45 | }
46 | });
47 | }
48 |
49 | /**
50 | * Tests whether the two objects are shallowly equivalent using the Immutable.is method.
51 | * If we pass in two objects that have properties that are vanilla (not Immutable) objects,
52 | * this method will always return false, therefore make sure you are passing in immutable
53 | * objects to your props.
54 | * @param {Object} item1 The first object to compare
55 | * @param {Object} item2 The second object to compare
56 | * @param {Object} component The component being examined
57 | * @param {Object} ignoreImmutableCheckKeys Objects to skip when checking if immutable
58 | * @return {Boolean} True if the objects are shallowly equavalent, else false.
59 | */
60 | function shallowEqualsImmutable(item1, item2, component, ignoreImmutableCheckKeys) {
61 | if (item1 === item2) {
62 | return true;
63 | }
64 | if (!item1 || !item2) {
65 | return false;
66 | }
67 |
68 | var i;
69 | var key;
70 | var ignoreImmutableCheck;
71 | var ignoreAllWarnings = component.ignoreAllWarnings;
72 | var item1Keys = Object.keys(item1);
73 | var item2Keys = Object.keys(item2);
74 | var item1Prop;
75 | var item2Prop;
76 |
77 | // Different key set length, no need to proceed..check it here because we still
78 | // want to check all of item2's objects to see if any are non-immutable.
79 | if (item1Keys.length !== item2Keys.length) {
80 | return false;
81 | }
82 | // check item2keys so that we can also check for any non-immutable objects
83 | for (i = 0; i < item2Keys.length; i++) {
84 | key = item2Keys[i];
85 | ignoreImmutableCheck = ignoreImmutableCheckKeys[key];
86 | item2Prop = item2[key];
87 | item1Prop = item1[key];
88 |
89 | if (!ignoreAllWarnings && !ignoreImmutableCheck) {
90 | checkNonImmutableObject(key, item2Prop, component);
91 | }
92 | if (ignoreImmutableCheck !== IGNORE_EQUALITY_CHECK_FLAG
93 | && (!item1.hasOwnProperty(key) || item1Prop !== item2Prop)) {
94 | return false;
95 | }
96 | }
97 |
98 | return true;
99 | }
100 |
101 | /**
102 | * Merge either an already defined object or a constructor object over the default
103 | * values. This function is used to create the ignoreImmutableCheck object
104 | * which has a 'props' and 'state' key.
105 | * @param {Object} defaultObject And object with default values.
106 | * @param {Object} config A config used by the mixin.
107 | * @param {String} objectName The name/key of the object we are creating.
108 | * @param {Object} component The component this object is being attached to.
109 | * @return {Object} The newly created object.
110 | */
111 | function mergeDefaultValues(defaultObject, config, objectName, component) {
112 | var objectToCreate = component[objectName]
113 | || component.constructor[objectName]
114 | || config[objectName]
115 | || {};
116 | // merge any custom configs over defaults
117 | objectToCreate.props = utils.merge(defaultObject.props, objectToCreate.props);
118 | objectToCreate.state = utils.merge(defaultObject.state, objectToCreate.state);
119 |
120 | // assign any values that are for both props and state.
121 | Object.keys(objectToCreate).forEach(function keyIterator(key) {
122 | if (key !== 'props' && key !== 'state') {
123 | var val = objectToCreate[key];
124 | objectToCreate.props[key] = val;
125 | objectToCreate.state[key] = val;
126 | }
127 | });
128 | return objectToCreate;
129 | }
130 |
131 | var defaults = {
132 | /**
133 | * Get default ignoreImmutableCheck objects. This is not a hardcoded object
134 | * since it might be mutable
135 | * @return {Object} by default avoid props.children
136 | */
137 | getIgnoreImmutableCheck: function () {
138 | return {
139 | props: {
140 | // Always ignore children props since it's special
141 | children: true
142 | }
143 | };
144 | },
145 |
146 | /**
147 | * Used as the default shouldComponentUpdate function. Checks whether the props/state of the
148 | * component has actually changed so that we know whether or not to run the render() method.
149 | * Since all state/props are immutable, we can use a simple reference check in the majority of cases.
150 | * @method shouldUpdate
151 | * @param {Object} nextProps The new props object for the component.
152 | * @param {Object} nextState The new state object for the component.
153 | * @return {Boolean} True if the component should run render(), else false.
154 | */
155 | shouldComponentUpdate: function shouldComponentUpdate(nextProps, nextState) {
156 | var ignoreImmutableCheck = this.ignoreImmutableCheck;
157 | var propsEqual = shallowEqualsImmutable(this.props, nextProps, this, ignoreImmutableCheck.props);
158 | var stateEqual = shallowEqualsImmutable(this.state, nextState, this, ignoreImmutableCheck.state);
159 | return !(stateEqual && propsEqual);
160 | },
161 |
162 | /**
163 | * A default onChange function that sets the the components state from the getStateOnChange
164 | * method. This is only set if a component does not implement its own onChange function.
165 | * @method defaultOnChange
166 | * @return {undefined} Does not return anything
167 | */
168 | onChange: function onChange() {
169 | if (this[GET_STATE_FUNCTION]) {
170 | this.setState(this[GET_STATE_FUNCTION].apply(this, arguments));
171 | }
172 | }
173 | };
174 |
175 | /**
176 | * React mixin for making components state/props immutable using the immutable.js library. This
177 | * mixin ensures that the state and props of the component always contain immutable objects, allowing
178 | * us to implement a fast version of shouldComponentUpdate (reducing render calls). The mixin
179 | * expects that the component uses the method 'getStateOnChange' instead of 'getInitialState'
180 | * in order to ensure that state is an immutable object. Additionally, this component overrides
181 | * the 'setState' method so that it works correctly with an immutable state.
182 | * @class ImmutableMixin
183 | */
184 |
185 | /**
186 | * A constructor function for the ComponentMixin that takes an optional config.
187 | * The config can specify values for 'ignoreImmutableCheck' and 'ignoreAllWarnings'.
188 | * @param {Object} config An optional config used by the mixin.
189 | * @return {Object} The mixin object
190 | */
191 | module.exports = function (config) {
192 | config = config || {};
193 | return {
194 | componentWillMount: function () {
195 | this.ignoreImmutableCheck = mergeDefaultValues(
196 | defaults.getIgnoreImmutableCheck(), config, IGNORE_IMMUTABLE_CHECK, this);
197 | this.ignoreAllWarnings = this.ignoreAllWarnings
198 | || this.constructor.ignoreAllWarnings
199 | || config.ignoreAllWarnings
200 | || false;
201 |
202 | // Set default methods if the there is no override
203 | this.onChange = this.onChange || defaults.onChange.bind(this);
204 | this.shouldComponentUpdate = this.shouldComponentUpdate || defaults.shouldComponentUpdate.bind(this);
205 | // Checks the props and state to raise warnings
206 | checkObjectProperties(this.props, this, this.ignoreImmutableCheck.props);
207 | checkObjectProperties(this.state, this, this.ignoreImmutableCheck.state);
208 | },
209 |
210 | getInitialState: function () {
211 | return this[GET_STATE_FUNCTION] ? this[GET_STATE_FUNCTION]() : {};
212 | }
213 | };
214 | };
215 |
--------------------------------------------------------------------------------
/lib/createImmutableStore.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, Yahoo Inc. All rights reserved.
3 | * Copyrights licensed under the New BSD License.
4 | * See the accompanying LICENSE file for terms.
5 | */
6 | 'use strict';
7 |
8 | var Immutable = require('immutable');
9 | var utils = require('../internals/utils');
10 | var createStore = require('fluxible/addons/createStore');
11 |
12 | function initialize() {
13 | this._state = Immutable.Map();
14 | }
15 |
16 | function rehydrate(state) {
17 | this._state = Immutable.fromJS(state);
18 | }
19 |
20 | function dehydrate() {
21 | return this._state;
22 | }
23 |
24 | function setState(newState, event, payload) {
25 | newState = Immutable.fromJS(newState);
26 |
27 | if (this._state.equals(newState)) {
28 | return false;
29 | }
30 |
31 | this._state = newState;
32 |
33 | if (event) {
34 | this.emit(event, payload);
35 | } else {
36 | this.emitChange(payload);
37 | }
38 |
39 | return true;
40 | }
41 |
42 | function mergeState(stateFragment, event, payload) {
43 | return this.setState(
44 | this._state.merge(stateFragment),
45 | event,
46 | payload
47 | );
48 | }
49 |
50 | function get(key, defaultValue) {
51 | return Array.isArray(key) ?
52 | this._state.getIn(key, defaultValue) :
53 | this._state.get(key, defaultValue);
54 | }
55 |
56 | /**
57 | * Helper for creating an immutable store class
58 | *
59 | * @method createStore
60 | * @param {Object} spec of the created Store class
61 | * @param {String} spec.storeName The name of the store
62 | * @param {Object} spec.handlers Hash of action name to method name of action handlers
63 | * @param {Function} [spec.initialize] Function called during construction for setting the default `_state` (optional).
64 | * @return {Store} Store class
65 | **/
66 | module.exports = function createImmutableStore(spec) {
67 | return createStore(utils.merge({
68 | initialize: initialize,
69 | rehydrate: rehydrate,
70 | dehydrate: dehydrate,
71 | setState: setState,
72 | mergeState: mergeState,
73 | get: get
74 | }, spec));
75 | };
76 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | createImmutableMixin: require('./createImmutableMixin'),
3 | createImmutableStore: require('./createImmutableStore')
4 | };
5 |
--------------------------------------------------------------------------------
/mixins/ImmutableMixin.js:
--------------------------------------------------------------------------------
1 | module.exports = require('../lib/createImmutableMixin')();
2 |
--------------------------------------------------------------------------------
/mixins/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ComponentMixin: require('./ImmutableMixin')
3 | };
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fluxible-immutable-utils",
3 | "author": "dhood@yahoo-inc.com",
4 | "description": "A collection of utilities that provide convenience methods for using Immutable.js with a fluxible based application",
5 | "version": "0.5.10",
6 | "main": "index.js",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/yahoo/fluxible-immutable-utils"
10 | },
11 | "scripts": {
12 | "lint": "eslint .",
13 | "pretest": "npm run lint",
14 | "test": "jenkins-mocha tests --recursive"
15 | },
16 | "peerDependencies": {
17 | "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0",
18 | "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
19 | },
20 | "dependencies": {
21 | "create-react-class": "^15.5.0",
22 | "fluxible": "^1.4.0",
23 | "fluxible-addons-react": "^0.2.12",
24 | "immutable": "^3.8.1",
25 | "inherits": "^2.0.3",
26 | "shallowequal": "^1.0.1"
27 | },
28 | "devDependencies": {
29 | "chai": "^4.0.2",
30 | "coveralls": "^2.13.1",
31 | "eslint": "^4.0.0",
32 | "filter-invalid-dom-props": "^1.0.0",
33 | "jenkins-mocha": "^4.1.2",
34 | "jsx-test": "^2.0.1",
35 | "pre-commit": "^1.2.2",
36 | "react": "^17.0.0",
37 | "react-dom": "^17.0.0",
38 | "sinon": "^2.3.4"
39 | },
40 | "pre-commit": [
41 | "test"
42 | ],
43 | "contributors": [
44 | "Mo Kouli ",
45 | "Marcelo Eden ",
46 | "Akshay Patel "
47 | ],
48 | "license": "BSD-3-Clause",
49 | "keywords": [
50 | "yahoo",
51 | "immutable",
52 | "immutablejs",
53 | "immutable.js",
54 | "fluxible",
55 | "react",
56 | "flux"
57 | ]
58 | }
59 |
--------------------------------------------------------------------------------
/tests/lib/ImmutableStore.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, Yahoo Inc. All rights reserved.
3 | * Copyrights licensed under the New BSD License.
4 | * See the accompanying LICENSE file for terms.
5 | */
6 | /*globals describe,it,beforeEach*/
7 | 'use strict';
8 |
9 | var ImmutableStore = require('../../lib/ImmutableStore');
10 | var expect = require('chai').expect;
11 |
12 | describe('ImmutableStore', function () {
13 | beforeEach(function () {
14 | this.store = new ImmutableStore();
15 | });
16 |
17 | describe('#constructor', function () {
18 | it('defines a immutable _state', function () {
19 | expect(this.store._state.toJS()).to.deep.equal({});
20 | });
21 | });
22 |
23 | describe('#rehydrate', function () {
24 | it('sets the state', function () {
25 | var state = {list: [1, 2, 3], error: null};
26 | this.store.rehydrate(state);
27 |
28 | expect(this.store._state.toJS()).to.deep.equal(state);
29 | });
30 | });
31 |
32 | var methods = ['dehydrate', 'getState'];
33 | methods.forEach(function (methodName) {
34 | describe('#' + methodName, function () {
35 | it('gets the initial state', function () {
36 | expect(this.store[methodName]()).to.deep.equal(this.store._state);
37 | expect(this.store[methodName]().toJS()).to.deep.equal({});
38 | });
39 |
40 | it('gets the rehydrated state', function () {
41 | var state = {list: [1, 2, 3], error: null};
42 | this.store.rehydrate(state);
43 |
44 | expect(this.store[methodName]()).to.deep.equal(this.store._state);
45 | });
46 | });
47 | });
48 |
49 | describe('#get', function () {
50 | beforeEach(function () {
51 | this.store.rehydrate({
52 | weapons: ['sword', 'bow', 'shotgun'],
53 | spells: {
54 | 'Fire Ball': 3,
55 | 'Arcane Intelect': 1
56 | }
57 | });
58 | });
59 |
60 | it('gets a part of the state', function () {
61 | expect(this.store.get('weapons').toJS()).to.deep.equal([
62 | 'sword', 'bow', 'shotgun'
63 | ]);
64 | });
65 |
66 | it('gets a nested part of the state', function () {
67 | expect(this.store.get(['weapons', 1])).to.equal('bow');
68 | expect(this.store.get(['spells', 'Fire Ball'])).to.equal(3);
69 | expect(this.store.get(['spells', 'Arcane Intelect'])).to.equal(1);
70 | });
71 | });
72 |
73 | describe('#setState', function () {
74 | it('updates the state and emits a "change" event', function (done) {
75 | this.store.on('change', function () {
76 | expect(this._state.toJS()).to.deep.equal({list: [1, 2, 3]});
77 | done();
78 | }.bind(this.store));
79 |
80 | this.store.setState(this.store._state.set('list', [1, 2, 3]));
81 | });
82 |
83 | it('updates the state and emit a custom event', function (done) {
84 | this.store.on('rename', function () {
85 | expect(this._state.toJS()).to.deep.equal({name: '_mo'});
86 | done();
87 | }.bind(this.store));
88 |
89 | this.store.setState(this.store._state.set('name', '_mo'), 'rename');
90 | });
91 |
92 | it('updates the state and passes a custom payload', function (done) {
93 | var payload = {name: '_mo', type: 'AnyPlayload'};
94 |
95 | this.store.on('rename', function (data) {
96 | expect(this._state.toJS()).to.deep.equal({name: '_mo'});
97 | expect(data).to.deep.equal(payload);
98 | done();
99 | }.bind(this.store));
100 |
101 | this.store.setState(this.store._state.set('name', '_mo'), 'rename', payload);
102 | });
103 |
104 | it('only emits the chage if there are changes', function (done) {
105 | var count = 0;
106 |
107 | this.store.emit = function () {
108 | count++;
109 | };
110 |
111 | this.store.setState(this.store._state.mergeDeep({list: [1, 2, 3]}));
112 | this.store.setState(this.store._state.mergeDeep({list: [1, 2, 3]}));
113 | this.store.setState(this.store._state.mergeDeep({list: [1, 2, 3]}));
114 |
115 | expect(this.store._state.toJS()).to.deep.equal({list: [1, 2, 3]});
116 | expect(count).to.equal(1);
117 | done();
118 | });
119 | });
120 |
121 | describe('#mergeState', function () {
122 | beforeEach(function () {
123 | this.store.setState(this.store._state.set('list1', [1, 2, 3]));
124 | });
125 |
126 | it('replaces the list with a new list', function (done) {
127 | this.store.on('change', function () {
128 | expect(this._state.toJS()).to.deep.equal({
129 | list1: [4]
130 | });
131 | done();
132 | }.bind(this.store));
133 |
134 | this.store.mergeState({'list1': [4]});
135 | });
136 |
137 | it('merges the state and emits a "change" event', function (done) {
138 | this.store.on('change', function () {
139 | expect(this._state.toJS()).to.deep.equal({
140 | list1: [1, 2, 3],
141 | list2: [4, 5, 6]
142 | });
143 | done();
144 | }.bind(this.store));
145 |
146 | this.store.mergeState({'list2': [4, 5, 6]});
147 | });
148 |
149 | it('merges the state and emit a custom event', function (done) {
150 | this.store.on('custom', function () {
151 | expect(this._state.toJS()).to.deep.equal({
152 | list1: [1, 2, 3],
153 | list2: [4, 5, 6]
154 | });
155 | done();
156 | }.bind(this.store));
157 |
158 | this.store.mergeState({'list2': [4, 5, 6]}, 'custom');
159 | });
160 |
161 | it('merges the state and passes a custom payload', function (done) {
162 | var payload = {name: '_mo', type: 'AnyPlayload'};
163 |
164 | this.store.on('change', function (data) {
165 | expect(this._state.toJS()).to.deep.equal({
166 | list1: [1, 2, 3],
167 | list2: [4, 5, 6]
168 | });
169 | expect(data).to.deep.equal(payload);
170 | done();
171 | }.bind(this.store));
172 |
173 | this.store.mergeState({'list2': [4, 5, 6]}, 'change', payload);
174 | });
175 |
176 | it('only emits the change if there the stateFragment is different', function (done) {
177 | var count = 0;
178 |
179 | this.store.emit = function () {
180 | count++;
181 | };
182 |
183 | this.store.mergeState({list2: [4, 5, 6]});
184 | this.store.mergeState({list2: [4, 5, 6]});
185 | this.store.mergeState({list2: [4, 5, 6]});
186 |
187 | expect(this.store._state.toJS()).to.deep.equal({
188 | list1: [1, 2, 3],
189 | list2: [4, 5, 6]
190 | });
191 | expect(count).to.equal(1);
192 | done();
193 | });
194 | });
195 | });
196 |
--------------------------------------------------------------------------------
/tests/lib/createImmutableContainer.js:
--------------------------------------------------------------------------------
1 | /*globals describe, it, beforeEach, afterEach */
2 |
3 | 'use strict';
4 | var Immutable = require('immutable');
5 | var React = require('react');
6 | var createImmutableContainer = require('../../lib/createImmutableContainer');
7 | var createReactClass = require('create-react-class');
8 | var createStore = require('fluxible/addons/createStore');
9 | var expect = require('chai').expect;
10 | var filterInvalidDOMProps = require('filter-invalid-dom-props').default
11 | var jsx = require('jsx-test');
12 | var sinon = require('sinon');
13 |
14 | describe('createImmutableContainer', function () {
15 | var DummyComponent = createReactClass({
16 | displayName: 'Dummy',
17 |
18 | render: function () {
19 | return React.createElement('div', filterInvalidDOMProps(this.props));
20 | }
21 | });
22 |
23 | var DummyStore = createStore({
24 | storeName: 'DummyStore'
25 | });
26 |
27 | beforeEach(function () {
28 | sinon.spy(console, 'warn');
29 | });
30 |
31 | afterEach(function () {
32 | console.warn.restore();
33 | });
34 |
35 | it('wraps the component without the store connector', function () {
36 | var Component = createImmutableContainer(DummyComponent);
37 |
38 | expect(Component.displayName).to.equal('Dummy:Immutable');
39 | });
40 |
41 | it('wraps the component with the store connector', function () {
42 | var Component = createImmutableContainer(DummyComponent, {
43 | stores: [DummyStore],
44 | getStateFromStores: {
45 | DummyStore: function () { }
46 | }
47 | });
48 |
49 | expect(Component.displayName).to.equal('storeConnector(Dummy:Immutable)');
50 | });
51 |
52 | describe('#componentWillMount', function () {
53 | var Component;
54 | beforeEach(function () {
55 | Component = createImmutableContainer(DummyComponent, {
56 | ignore: ['data-items']
57 | });
58 | });
59 |
60 | it('raise warnings if non immutable props are passed', function () {
61 | jsx.renderComponent(Component, {stuff: [1, 2, 3]});
62 |
63 | expect(
64 | console.warn.calledWith('Component "Dummy:Immutable" received non-immutable object for "stuff"')
65 | ).to.equal(true);
66 | });
67 |
68 | it('bypasses certain fields if they are ignored', function () {
69 | jsx.renderComponent(Component, {'data-items': [1, 2, 3]});
70 | expect(console.warn.callCount).to.equal(0);
71 | });
72 |
73 | it('raises a warning for each non-immutable object', function () {
74 | jsx.renderComponent(Component, {
75 | items: [1, 2, 3],
76 | stuff: {},
77 | map: Immutable.Map(),
78 | number: 1,
79 | name: 'something'
80 | });
81 | expect(console.warn.callCount).to.equal(2);
82 | });
83 |
84 | it('should never warn if ignoreAllWarnings is true', function () {
85 | var Component2 = createImmutableContainer(DummyComponent, {
86 | ignoreWarnings: true
87 | });
88 |
89 | jsx.renderComponent(Component2, {
90 | items: [1, 2, 3],
91 | nonImmutable: {}
92 | });
93 |
94 | expect(console.warn.callCount).to.equal(0);
95 | });
96 | });
97 |
98 | describe('#componentWillUpdate', function () {
99 | var Component = createImmutableContainer(DummyComponent);
100 | var component = jsx.renderComponent(Component, {
101 | items: [1, 2, 3],
102 | stuff: {},
103 | map: Immutable.Map(),
104 | number: 1,
105 | name: 'something'
106 | });
107 |
108 | it('raises a warning for each non-imutable object', function () {
109 | component.componentWillUpdate();
110 | expect(console.warn.callCount).to.equal(2);
111 | });
112 | });
113 |
114 | describe('#shouldComponentUpdate', function () {
115 | var someMap = Immutable.Map();
116 | var Component = createImmutableContainer(DummyComponent);
117 |
118 | beforeEach(function () {
119 | this.component = jsx.renderComponent(Component, {
120 | name: 'Bilbo',
121 | map: someMap
122 | });
123 | });
124 |
125 | it('should return false if props are equal', function () {
126 | expect(this.component.shouldComponentUpdate({
127 | name: 'Bilbo',
128 | map: someMap
129 | })).to.equal(false);
130 | });
131 |
132 | it('should return true if any prop changes', function () {
133 | expect(this.component.shouldComponentUpdate({
134 | name: 'Frodo',
135 | map: someMap
136 | })).to.equal(true);
137 | });
138 |
139 | it('should return true if any prop is removed', function () {
140 | expect(this.component.shouldComponentUpdate({
141 | name: 'Bilbo'
142 | })).to.equal(true);
143 | });
144 |
145 | it('should return true if a new prop is passed', function () {
146 | expect(this.component.shouldComponentUpdate({
147 | name: 'Bilbo',
148 | map: someMap,
149 | n: 1
150 | })).to.equal(true);
151 | });
152 | });
153 | });
154 |
--------------------------------------------------------------------------------
/tests/lib/createImmutableMixin.js:
--------------------------------------------------------------------------------
1 | /*globals describe,it,beforeEach,afterEach*/
2 |
3 | 'use strict';
4 |
5 | var createImmutableMixin = require('../../lib/createImmutableMixin');
6 | var createReactClass = require('create-react-class');
7 | var expect = require('chai').expect;
8 | var jsx = require('jsx-test');
9 | var sinon = require('sinon');
10 |
11 | describe('createImmutableMixin', function () {
12 | var config = {
13 | ignoreImmutableCheck: {
14 | props: {
15 | data: 'SKIP_SHOULD_UPDATE'
16 | },
17 | state: {
18 | foo: true
19 | }
20 | }
21 | };
22 | var CustomImmutableMixin = createImmutableMixin(config);
23 |
24 | beforeEach(function () {
25 | sinon.spy(console, 'warn');
26 | });
27 |
28 | afterEach(function () {
29 | console.warn.restore();
30 | });
31 |
32 | it('should apply configs if we use createComponentMixn', function () {
33 | var state = {
34 | foo: {},
35 | baz: {}
36 | };
37 | var Component = createReactClass({
38 | displayName: 'MyComponent',
39 | mixins: [CustomImmutableMixin],
40 | render: function () {
41 | return null;
42 | },
43 | getStateOnChange: function () {
44 | return state;
45 | }
46 | });
47 | var props = {
48 | data: {}
49 | };
50 | var component = jsx.renderComponent(Component, {data: false});
51 | expect(component.shouldComponentUpdate(props, state)).to.equal(false);
52 | expect(console.warn.callCount).to.equal(1);
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/tests/lib/createImmutableStore.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, Yahoo Inc. All rights reserved.
3 | * Copyrights licensed under the New BSD License.
4 | * See the accompanying LICENSE file for terms.
5 | */
6 | /*globals describe,it,beforeEach*/
7 | 'use strict';
8 |
9 | var createImmutableStore = require('../../lib/createImmutableStore');
10 | var expect = require('chai').expect;
11 |
12 | describe('createImmutableStore', function () {
13 | var Store = createImmutableStore({
14 | storeName: 'ImmutableStore'
15 | });
16 |
17 | beforeEach(function () {
18 | this.store = new Store();
19 | });
20 |
21 | describe('#initialize', function () {
22 | it('defines a immutable _state', function () {
23 | expect(this.store._state.toJS()).to.deep.equal({});
24 | });
25 | });
26 |
27 | describe('#rehydrate', function () {
28 | it('sets the state', function () {
29 | var state = {list: [1, 2, 3], error: null};
30 | this.store.rehydrate(state);
31 |
32 | expect(this.store._state.toJS()).to.deep.equal(state);
33 | });
34 | });
35 |
36 | describe('#dehydrate', function () {
37 | it('gets the initial state', function () {
38 | expect(this.store.dehydrate()).to.deep.equal(this.store._state);
39 | expect(this.store.dehydrate().toJS()).to.deep.equal({});
40 | });
41 |
42 | it('gets the rehydrated state', function () {
43 | var state = {list: [1, 2, 3], error: null};
44 | this.store.rehydrate(state);
45 |
46 | expect(this.store.dehydrate()).to.deep.equal(this.store._state);
47 | });
48 | });
49 |
50 | describe('#get', function () {
51 | beforeEach(function () {
52 | this.store.rehydrate({
53 | weapons: ['sword', 'bow', 'shotgun'],
54 | spells: {
55 | 'Fire Ball': 3,
56 | 'Arcane Intelect': 1
57 | }
58 | });
59 | });
60 |
61 | it('gets a part of the state', function () {
62 | expect(this.store.get('weapons').toJS()).to.deep.equal([
63 | 'sword', 'bow', 'shotgun'
64 | ]);
65 | });
66 |
67 | it('gets a nested part of the state', function () {
68 | expect(this.store.get(['weapons', 1])).to.equal('bow');
69 | expect(this.store.get(['spells', 'Fire Ball'])).to.equal(3);
70 | expect(this.store.get(['spells', 'Arcane Intelect'])).to.equal(1);
71 | });
72 |
73 | it('returns default value if part in state is not defined', function () {
74 | expect(this.store.get(['weapons', 3], 'knife')).to.equal('knife');
75 | expect(this.store.get('casts', 'Magic')).to.equal('Magic');
76 | });
77 | });
78 |
79 | describe('#setState', function () {
80 | it('updates the state and emits a "change" event', function (done) {
81 | this.store.on('change', function () {
82 | expect(this._state.toJS()).to.deep.equal({list: [1, 2, 3]});
83 | done();
84 | }.bind(this.store));
85 |
86 | this.store.setState(this.store._state.set('list', [1, 2, 3]));
87 | });
88 |
89 | it('updates the state and emit a custom event', function (done) {
90 | this.store.on('rename', function () {
91 | expect(this._state.toJS()).to.deep.equal({name: '_mo'});
92 | done();
93 | }.bind(this.store));
94 |
95 | this.store.setState(this.store._state.set('name', '_mo'), 'rename');
96 | });
97 |
98 | it('updates the state and passes a custom payload', function (done) {
99 | var payload = {name: '_mo', type: 'AnyPlayload'};
100 |
101 | this.store.on('rename', function (data) {
102 | expect(this._state.toJS()).to.deep.equal({name: '_mo'});
103 | expect(data).to.deep.equal(payload);
104 | done();
105 | }.bind(this.store));
106 |
107 | this.store.setState(this.store._state.set('name', '_mo'), 'rename', payload);
108 | });
109 |
110 | it('only emits the chage if there are changes', function (done) {
111 | var count = 0;
112 |
113 | this.store.emit = function () {
114 | count++;
115 | };
116 |
117 | this.store.setState(this.store._state.mergeDeep({list: [1, 2, 3]}));
118 | this.store.setState(this.store._state.mergeDeep({list: [1, 2, 3]}));
119 | this.store.setState(this.store._state.mergeDeep({list: [1, 2, 3]}));
120 |
121 | expect(this.store._state.toJS()).to.deep.equal({list: [1, 2, 3]});
122 | expect(count).to.equal(1);
123 | done();
124 | });
125 | });
126 |
127 | describe('#mergeState', function () {
128 | beforeEach(function () {
129 | this.store.setState(this.store._state.set('list1', [1, 2, 3]));
130 | });
131 |
132 | it('replaces the list with a new list', function (done) {
133 | this.store.on('change', function () {
134 | expect(this._state.toJS()).to.deep.equal({
135 | list1: [4]
136 | });
137 | done();
138 | }.bind(this.store));
139 |
140 | this.store.mergeState({'list1': [4]});
141 | });
142 |
143 | it('merges the state and emits a "change" event', function (done) {
144 | this.store.on('change', function () {
145 | expect(this._state.toJS()).to.deep.equal({
146 | list1: [1, 2, 3],
147 | list2: [4, 5, 6]
148 | });
149 | done();
150 | }.bind(this.store));
151 |
152 | this.store.mergeState({'list2': [4, 5, 6]});
153 | });
154 |
155 | it('merges the state and emit a custom event', function (done) {
156 | this.store.on('custom', function () {
157 | expect(this._state.toJS()).to.deep.equal({
158 | list1: [1, 2, 3],
159 | list2: [4, 5, 6]
160 | });
161 | done();
162 | }.bind(this.store));
163 |
164 | this.store.mergeState({'list2': [4, 5, 6]}, 'custom');
165 | });
166 |
167 | it('merges the state and passes a custom payload', function (done) {
168 | var payload = {name: '_mo', type: 'AnyPlayload'};
169 |
170 | this.store.on('change', function (data) {
171 | expect(this._state.toJS()).to.deep.equal({
172 | list1: [1, 2, 3],
173 | list2: [4, 5, 6]
174 | });
175 | expect(data).to.deep.equal(payload);
176 | done();
177 | }.bind(this.store));
178 |
179 | this.store.mergeState({'list2': [4, 5, 6]}, 'change', payload);
180 | });
181 |
182 | it('only emits the change if there the stateFragment is different', function (done) {
183 | var count = 0;
184 |
185 | this.store.emit = function () {
186 | count++;
187 | };
188 |
189 | this.store.mergeState({list2: [4, 5, 6]});
190 | this.store.mergeState({list2: [4, 5, 6]});
191 | this.store.mergeState({list2: [4, 5, 6]});
192 |
193 | expect(this.store._state.toJS()).to.deep.equal({
194 | list1: [1, 2, 3],
195 | list2: [4, 5, 6]
196 | });
197 | expect(count).to.equal(1);
198 | done();
199 | });
200 | });
201 | });
202 |
--------------------------------------------------------------------------------
/tests/mixins/ImmutableMixin.js:
--------------------------------------------------------------------------------
1 | /* eslint no-warning-comments: off */
2 | /*globals describe,it,beforeEach,afterEach*/
3 |
4 | 'use strict';
5 |
6 | var Immutable = require('immutable');
7 | var ImmutableMixin = require('../../mixins/ImmutableMixin');
8 | var React = require('react');
9 | var createReactClass = require('create-react-class');
10 | var expect = require('chai').expect;
11 | var jsx = require('jsx-test');
12 | var sinon = require('sinon');
13 |
14 | describe('ImmutableMixin', function () {
15 | describe('#objectsToIgnore', function () {
16 | beforeEach(function () {
17 | sinon.spy(console, 'warn');
18 | });
19 |
20 | afterEach(function () {
21 | console.warn.restore();
22 | });
23 |
24 | it('should bypass certain props fields if they are ignored', function () {
25 | var Component = createReactClass({
26 | displayName: 'MyComponent',
27 | mixins: [ImmutableMixin],
28 | // TODO: simplify ignoreImmutableCheck
29 | ignoreImmutableCheck: {
30 | props: {
31 | data: true
32 | }
33 | },
34 | render: function () {
35 | return null;
36 | }
37 | });
38 |
39 | jsx.renderComponent(Component, {data: {list: [1, 2, 3]}});
40 | expect(console.warn.callCount).to.equal(0);
41 | });
42 |
43 | it('should bypass fields in both props and state if they at the top level', function () {
44 | var Component = createReactClass({
45 | displayName: 'MyComponent',
46 | mixins: [ImmutableMixin],
47 | ignoreImmutableCheck: {
48 | data: true
49 | },
50 | getStateOnChange: function () {
51 | return {
52 | data: true
53 | };
54 | },
55 | render: function () {
56 | return null;
57 | }
58 | });
59 |
60 | var component = jsx.renderComponent(Component, {data: {}});
61 | expect(console.warn.callCount).to.equal(0);
62 | expect(component.shouldComponentUpdate({}, {data: {}})).to.equal(true);
63 | expect(console.warn.callCount).to.equal(0);
64 | });
65 |
66 | it('should ignore certain props/state fields if they are marked SKIP_SHOULD_UPDATE', function () {
67 | var Component = createReactClass({
68 | displayName: 'MyComponent',
69 | mixins: [ImmutableMixin],
70 | ignoreImmutableCheck: {
71 | props: {
72 | data: 'SKIP_SHOULD_UPDATE'
73 | }
74 | },
75 | render: function () {
76 | return null;
77 | }
78 | });
79 | var props = {
80 | data: true
81 | };
82 | var component = jsx.renderComponent(Component, {data: {}});
83 | expect(component.shouldComponentUpdate(props, {})).to.equal(false);
84 | expect(console.warn.callCount).to.equal(0);
85 | });
86 |
87 | it('should warn if next state is mutable', function (done) {
88 | var Component = createReactClass({
89 | displayName: 'MyComponent',
90 | mixins: [ImmutableMixin],
91 | render: function () {
92 | return null;
93 | },
94 | getStateOnChange: function () {
95 | return {
96 | testData: Immutable.Map()
97 | };
98 | }
99 | });
100 |
101 | var comp = jsx.renderComponent(Component, {});
102 | comp.setState({testData: {list: [1, 2, 3]}}, function () {
103 | expect(
104 | console.warn.calledWith('WARN: component: MyComponent received non-immutable object: testData')
105 | ).to.equal(true);
106 | done();
107 | });
108 | });
109 |
110 | it('should never warn if ignoreAllWarnings is true', function (done) {
111 | var Component = createReactClass({
112 | displayName: 'MyComponent',
113 | mixins: [ImmutableMixin],
114 | statics: {
115 | ignoreAllWarnings: true
116 | },
117 | render: function () {
118 | return null;
119 | }
120 | });
121 |
122 | var comp = jsx.renderComponent(Component, {});
123 | comp.setState({testData: {list: [1, 2, 3]}}, function () {
124 | expect(console.warn.callCount).to.equal(0);
125 | done();
126 | });
127 | });
128 |
129 | it('should bypass certain state fields if are ignored', function (done) {
130 | var Component = createReactClass({
131 | displayName: 'MyComponent',
132 | mixins: [ImmutableMixin],
133 | ignoreImmutableCheck: {
134 | state: {
135 | testData: true
136 | }
137 | },
138 | render: function () {
139 | return null;
140 | }
141 | });
142 |
143 | var comp = jsx.renderComponent(Component, {});
144 | comp.setState({testData: {list: [1, 2, 3]}}, function () {
145 | expect(console.warn.callCount).to.equal(0);
146 | done();
147 | });
148 | });
149 |
150 | it('should merge w/ default certain state fields if are ignored', function (done) {
151 | var Component = createReactClass({
152 | displayName: 'MyComponent',
153 | mixins: [ImmutableMixin],
154 | ignoreImmutableCheck: {
155 | state: {
156 | testData: true
157 | }
158 | },
159 | render: function () {
160 | return React.createElement('div', {}, this.props.children);
161 | }
162 | });
163 |
164 | var comp = jsx.renderComponent(Component, {}, ['foo', 'bar']);
165 | comp.setState({testData: {list: [1, 2, 3]}}, function () {
166 | expect(console.warn.callCount).to.equal(0);
167 | done();
168 | });
169 | });
170 | });
171 |
172 | describe('#componentWillMount', function () {
173 | beforeEach(function () {
174 | sinon.spy(console, 'warn');
175 | });
176 |
177 | afterEach(function () {
178 | console.warn.restore();
179 | });
180 |
181 | it('should raise warnings if non immutable props are passed', function () {
182 | var Component = createReactClass({
183 | displayName: 'MyComponent',
184 | mixins: [ImmutableMixin],
185 | render: function () {
186 | return null;
187 | }
188 | });
189 |
190 | jsx.renderComponent(Component, {data: {list: [1, 2, 3]}});
191 | expect(
192 | console.warn.calledWith('WARN: component: MyComponent received non-immutable object: data')
193 | ).to.equal(true);
194 | });
195 |
196 | it('should not raise warnings for children', function () {
197 | var Component = createReactClass({
198 | displayName: 'MyComponent',
199 | mixins: [ImmutableMixin],
200 | render: function () {
201 | return React.createElement('div', {}, this.props.children);
202 | }
203 | });
204 |
205 | jsx.renderComponent(Component, {}, ['foo', 'bar']);
206 | expect(console.warn.callCount).to.equal(0);
207 | });
208 |
209 | it('does not raise warning if props or state is null', function () {
210 | var Component = createReactClass({
211 | displayName: 'MyComponent',
212 | mixins: [ImmutableMixin],
213 | getInitialState: function () {
214 | return null;
215 | },
216 | getStateOnChange: function () {
217 | return null;
218 | },
219 | render: function () {
220 | return null;
221 | }
222 | });
223 |
224 | jsx.renderComponent(Component, null);
225 | expect(console.warn.callCount).to.equal(0);
226 | });
227 |
228 | it('should raise warkings if there are non immutable states', function () {
229 | var Component = createReactClass({
230 | displayName: 'YComponent',
231 | mixins: [ImmutableMixin],
232 | getInitialState: function () {
233 | return {list: [1, 2, 3]};
234 | },
235 | render: function () {
236 | return null;
237 | }
238 | });
239 |
240 | jsx.renderComponent(Component, {});
241 | expect(
242 | console.warn.calledWith('WARN: component: YComponent received non-immutable object: list')
243 | ).to.equal(true);
244 | });
245 | });
246 |
247 | describe('#getStateOnChange', function () {
248 | it('should merge the getInitialState with getStateOnChange', function () {
249 | var Component = createReactClass({
250 | mixins: [ImmutableMixin],
251 | getStateOnChange: function () {
252 | return {foo: 'bar'};
253 | },
254 | getInitialState: function () {
255 | return {bar: 'foo'};
256 | },
257 | render: function () {
258 | return null;
259 | }
260 | });
261 |
262 | var component = jsx.renderComponent(Component, {});
263 |
264 | expect(component.state).to.deep.equal({
265 | bar: 'foo',
266 | foo: 'bar'
267 | });
268 | });
269 |
270 | it('should call getStateOnChange from onChange correctly', function (done) {
271 | var Component = createReactClass({
272 | mixins: [ImmutableMixin],
273 | getStateOnChange: function (foo) {
274 | if (foo) {
275 | done();
276 | }
277 | return {};
278 | },
279 | render: function () {
280 | return null;
281 | }
282 | });
283 |
284 | var component = jsx.renderComponent(Component, {});
285 | var onChange = component.onChange;
286 | onChange(true);
287 | });
288 |
289 | it('should call getStateOnChange to initialize state', function (done) {
290 | var Component = createReactClass({
291 | mixins: [ImmutableMixin],
292 | getStateOnChange: function () {
293 | done();
294 | return {};
295 | },
296 | render: function () {
297 | return null;
298 | }
299 | });
300 |
301 | jsx.renderComponent(Component, {});
302 | });
303 |
304 | it('shouldn\'t crash if getStateOnChange is not there', function () {
305 | var Component = createReactClass({
306 | mixins: [ImmutableMixin],
307 | render: function () {
308 | return null;
309 | }
310 | });
311 | var component = jsx.renderComponent(Component, {});
312 |
313 | // This shouldn't throw
314 | component.onChange({foo: 'bar'});
315 | });
316 |
317 | it('should pass onChange arguments to getStateOnChange', function () {
318 | var Component = createReactClass({
319 | mixins: [ImmutableMixin],
320 | getStateOnChange: function (data) {
321 | return data || {};
322 | },
323 | render: function () {
324 | return null;
325 | }
326 | });
327 |
328 | var component = jsx.renderComponent(Component, {});
329 | component.onChange({foo: 'bar'});
330 | expect(component.state).to.deep.equal({foo: 'bar'});
331 | });
332 | });
333 |
334 | describe('#shouldComponentUpdate', function () {
335 | var component;
336 | var props;
337 | var state;
338 |
339 | beforeEach(function () {
340 | props = {foo: 'bar', list: Immutable.fromJS(['baz', 'foo'])};
341 | state = {list: Immutable.fromJS(['baz', 'foo'])};
342 |
343 | var Component = createReactClass({
344 | mixins: [ImmutableMixin],
345 | getStateOnChange: function () {
346 | return state;
347 | },
348 | render: function () {
349 | return null;
350 | }
351 | });
352 |
353 | component = jsx.renderComponent(Component, props);
354 | });
355 |
356 | function assertComponentUpdate(newProps, newState, expected) {
357 | expect(
358 | component.shouldComponentUpdate(newProps, newState)
359 | ).to.equal(expected);
360 | }
361 |
362 | it('should return false if props/state are equal', function () {
363 | assertComponentUpdate(props, state, false);
364 | assertComponentUpdate({foo: 'bar', list: props.list}, state, false);
365 | assertComponentUpdate(props, {list: state.list}, false);
366 | });
367 |
368 | it('should return true if a current prop value is changed', function () {
369 | assertComponentUpdate({foo: 'fubar', list: props.list}, state, true);
370 | assertComponentUpdate({foo: 'bar', list: state.list}, state, true);
371 | assertComponentUpdate({}, state, true);
372 | });
373 |
374 | it('should return true if a new prop value is added', function () {
375 | assertComponentUpdate(props, state, false);
376 | props.test = 'baz';
377 | assertComponentUpdate(props, state, true);
378 | });
379 |
380 | it('should return true if a new prop value is removed', function () {
381 | assertComponentUpdate(props, state, false);
382 | delete props.foo;
383 | assertComponentUpdate(props, state, true);
384 | });
385 |
386 | it('should return true if state is changed', function () {
387 | assertComponentUpdate(props, {list: props.list}, true);
388 | assertComponentUpdate(props, {foo: 'bar', list: state.list}, true);
389 | });
390 |
391 | it('should return true if state is null', function () {
392 | assertComponentUpdate(props, null, true);
393 | assertComponentUpdate(props, {foo: 'bar', list: state.list}, true);
394 | });
395 |
396 | it('allows the shouldComponentUpdate to be overridden', function () {
397 | var Component = createReactClass({
398 | mixins: [ImmutableMixin],
399 | shouldComponentUpdate: function () {
400 | return 'override';
401 | },
402 | render: function () {
403 | return null;
404 | }
405 | });
406 |
407 | component = jsx.renderComponent(Component, props);
408 | assertComponentUpdate({}, {}, 'override');
409 | });
410 | });
411 | });
412 |
--------------------------------------------------------------------------------