, ` +
23 | `or explicitly pass "falcorStore" as a prop to "${this.constructor.displayName}".`
24 | );
25 | }
26 |
27 | componentDidMount() {
28 | this.trySubscribe();
29 | }
30 |
31 | componentWillUnmount() {
32 | this.tryUnsubscribe();
33 | }
34 |
35 | trySubscribe() {
36 | if (!this.unsubscribe) {
37 | this.unsubscribe = this.falcorStore.subscribe(::this.handleChange);
38 | this.handleChange();
39 | }
40 | }
41 |
42 | handleChange() {
43 | const { wrappedInstance } = this.refs;
44 |
45 | if (!this.unsubscribe) return;
46 | if (!(typeof wrappedInstance.fetchFalcorDeps === 'function')) return;
47 |
48 | wrappedInstance.fetchFalcorDeps().then(noop);
49 | }
50 |
51 | tryUnsubscribe() {
52 | if (this.unsubscribe) {
53 | this.unsubscribe();
54 | this.unsubscribe = null;
55 | }
56 | }
57 |
58 | render() {
59 | return (
60 |
65 | );
66 | }
67 | }
68 |
69 | ReduxFalcor.displayName = `ReduxFalcor(${getDisplayName(WrappedComponent)})`;
70 | ReduxFalcor.WrappedComponent = WrappedComponent;
71 |
72 | ReduxFalcor.propTypes = {
73 | falcorStore: PropTypes.object,
74 | falcor: PropTypes.object,
75 | };
76 |
77 | ReduxFalcor.contextTypes = {
78 | falcorStore: PropTypes.object.isRequired,
79 | falcor: PropTypes.object.isRequired,
80 | };
81 |
82 | return hoistStatics(ReduxFalcor, WrappedComponent);
83 | }
84 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as reducer } from './components/duck';
2 | export { default as FalcorProvider } from './components/FalcorProvider';
3 | export { default as reduxFalcor } from './components/reduxFalcor';
4 |
--------------------------------------------------------------------------------
/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ekosz/redux-falcor/8d5b0ec450b8ddf2cf0ca6e71f60e494c9e1cb73/static/logo.png
--------------------------------------------------------------------------------
/test/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ekosz/redux-falcor/8d5b0ec450b8ddf2cf0ca6e71f60e494c9e1cb73/test/.gitkeep
--------------------------------------------------------------------------------
/test/integration/appContainer.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { reduxFalcor } from '../../src/index.js';
4 | import App from '../src/app.js';
5 |
6 | class AppContainer extends Component {
7 | // fetchFalcorDeps() {
8 | // return Promise.resolve();
9 | // }
10 |
11 | render() {
12 | return (
13 |
17 | );
18 | }
19 | }
20 |
21 | function mapStateToProps(state) {
22 | return {
23 | people: state.falcor.people
24 | };
25 | }
26 |
27 | export default connect(
28 | mapStateToProps,
29 | )(reduxFalcor(AppContainer));
30 |
--------------------------------------------------------------------------------
/test/integration/main.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import { FalcorProvider } from '../../src/index';
4 | import { falcor } from '../src/model';
5 | import {store} from './store';
6 | import AppContainer from './appContainer';
7 |
8 |
9 | const main = (
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
17 | export default main;
18 |
--------------------------------------------------------------------------------
/test/integration/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers } from 'redux';
2 | import { reducer as falcorReducer } from '../../src/index.js';
3 |
4 | const reducers = combineReducers({
5 | falcor: falcorReducer
6 | });
7 |
8 | export const store = createStore(reducers);
9 |
--------------------------------------------------------------------------------
/test/root.js:
--------------------------------------------------------------------------------
1 | import jsdom from 'jsdom';
2 |
3 | function noop() {}
4 |
5 | before(function() {
6 | // disable react warnings!
7 | console.error = noop;
8 | // load jsdom
9 | const doc = jsdom.jsdom('');
10 | global.document = doc;
11 | global.window = doc.defaultView;
12 | global.navigator = window.navigator;
13 | });
14 |
--------------------------------------------------------------------------------
/test/src/app.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class App extends React.Component {
4 |
5 | constructor() {
6 | super();
7 | this.fetchFalcorDeps = this.fetchFalcorDeps.bind(this);
8 | }
9 |
10 | fetchFalcorDeps() {
11 | return Promise.resolve();
12 | }
13 |
14 | render() {
15 | return (
16 | My favorite movie is {this.props.movie}. - {this.props.name}
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/test/src/model.js:
--------------------------------------------------------------------------------
1 | import { Model } from 'falcor';
2 |
3 | const cache = {
4 | people: [{
5 | name: 'Dan',
6 | movie: 'Redux'
7 | }]
8 | };
9 |
10 | const falcor = new Model({
11 | cache
12 | });
13 |
14 | export {
15 | cache,
16 | falcor
17 | };
18 |
--------------------------------------------------------------------------------
/test/test_create_store.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import { mock, spy } from 'sinon';
3 | import createStore from '../src/components/createStore.js';
4 |
5 | describe('createStore', function() {
6 | const reduxStore = {
7 | dispatch() {}
8 | };
9 |
10 | it('include subscribe and trigger', function() {
11 | const result = createStore();
12 | assert.ok(result);
13 | assert.ok(result.subscribe);
14 | assert.ok(result.trigger);
15 | });
16 |
17 | it('uses redux store for dispatch', function() {
18 | const reduxStoreMock = mock(reduxStore);
19 | reduxStoreMock.expects('dispatch').once();
20 | const result = createStore(reduxStore);
21 | result.trigger('cache');
22 | reduxStoreMock.verify();
23 | });
24 |
25 | it('returns cache', function() {
26 | const store = createStore(reduxStore);
27 | const cache = { message: 'Test' };
28 | const result = store.trigger(cache);
29 | assert.equal(result, cache);
30 | });
31 |
32 | it('returns an unsubscribe function', function() {
33 | const store = createStore();
34 | const fn = store.subscribe();
35 | assert.equal(typeof fn, 'function');
36 | assert.equal(fn.name, 'unsubscribe');
37 | });
38 |
39 | it('is called when trigger is invoked', function() {
40 | const store = createStore(reduxStore);
41 | const listener = spy();
42 | store.subscribe(listener);
43 | store.trigger('cache');
44 | assert(listener.calledOnce);
45 | });
46 |
47 | it('has a listener that unsubscribes', function() {
48 | const store = createStore(reduxStore);
49 | const listener = spy();
50 | const unsubscribe = store.subscribe(listener);
51 | unsubscribe();
52 | store.trigger('cache');
53 | assert(!listener.called);
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/test/test_duck.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import reduxFalcorReducer, { update } from '../src/components/duck.js';
3 |
4 | describe('duck', function() {
5 | describe('update', function() {
6 | it('has the right type', function() {
7 | const action = update();
8 | assert.equal('redux-falcor/UPDATE', action.type);
9 | });
10 |
11 | it('has the payload', function() {
12 | const cache = {
13 | name: 'Luke Skywalker',
14 | movie: 'Star Wars'
15 | };
16 | const action = update(cache);
17 | assert.equal(action.payload, cache);
18 | });
19 | });
20 |
21 | describe('reduxFalcorReducer', function() {
22 | describe('default action', function() {
23 | it('should have type', function() {
24 | assert.throws(() => {
25 | reduxFalcorReducer();
26 | });
27 | });
28 |
29 | it('should initialize state', function() {
30 | const state = reduxFalcorReducer(undefined, { type: 'INIT' });
31 | assert.deepEqual(state, {});
32 | });
33 | });
34 |
35 | describe('update action', function() {
36 | it('returns payload', function() {
37 | const payload = {
38 | name: 'Luke Skywalker',
39 | movie: 'Star Wars'
40 | };
41 | const action = {
42 | type: 'redux-falcor/UPDATE',
43 | payload
44 | };
45 | const state = reduxFalcorReducer(undefined, action);
46 | assert.deepEqual(state, payload);
47 | });
48 | });
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/test/test_falcor_provider.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import assert from 'assert';
3 | import { Model } from 'falcor';
4 | import { shallow, mount } from 'enzyme';
5 | import { mock, spy, stub } from 'sinon';
6 | import { FalcorProvider } from '../src/index';
7 | import App from './src/app';
8 | import { cache } from './src/model';
9 |
10 | function noop() {}
11 |
12 | describe('FalcorProvider', function() {
13 | describe('initialization', function() {
14 | it('requires falcor prop', function() {
15 | assert.throws(() => shallow());
16 | });
17 |
18 | it('should have one child', function() {
19 | const falcor = new Model({ cache });
20 | assert.throws(() => shallow(), /Invariant Violation:/);
21 | });
22 |
23 | it('requires redux store', function() {
24 | assert.throws(() => shallow());
25 | });
26 | });
27 |
28 | describe('changing a model', function() {
29 | let store;
30 | let component;
31 | let wrapper;
32 | let falcor;
33 |
34 | before(function() {
35 | store = { dispatch: noop };
36 | falcor = new Model({ cache });
37 | component = ;
38 | wrapper = shallow(component);
39 | });
40 |
41 | it('renders child', function() {
42 | assert.ok(wrapper.some('App'));
43 | });
44 |
45 | it('gets the falcor model', function(done) {
46 | const falcorMock = mock(falcor);
47 | falcorMock.expects('getCache').once();
48 | falcor._root.onChange();
49 | setTimeout(() => {
50 | falcorMock.verify();
51 | falcorMock.restore();
52 | done();
53 | }, 60);
54 | });
55 |
56 | it('dispatches to the store', function(done) {
57 | const storeMock = mock(store);
58 | storeMock.expects('dispatch').once();
59 | falcor._root.onChange();
60 | setTimeout(() => {
61 | storeMock.verify();
62 | storeMock.restore();
63 | done();
64 | }, 60);
65 | });
66 | });
67 |
68 | describe('mount', function() {
69 | it('sets the context', function() {
70 | const store = { dispatch: noop };
71 | const falcor = new Model({ cache });
72 | const component = ;
73 | const providerMock = mock(FalcorProvider.prototype);
74 | providerMock.expects('getChildContext').once();
75 | const wrapper = mount(component);
76 | providerMock.verify();
77 | providerMock.restore();
78 | });
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/test/test_integration.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import assert from 'assert';
3 | import { shallow, mount } from 'enzyme';
4 | import Main from './integration/main';
5 | import { cache, falcor } from './src/model';
6 |
7 | describe('integration', function() {
8 | let wrapper;
9 |
10 | before(function() {
11 | wrapper = mount(Main);
12 | });
13 |
14 | describe('mount', function() {
15 | it('runs without exceptions', function(){
16 | assert.ok(wrapper);
17 | });
18 |
19 | it('renders cache', function(done) {
20 | falcor._root.onChange();
21 | setTimeout(() => {
22 | const elements = wrapper.find('span');
23 | assert.equal(elements.length, 2);
24 | assert.equal(elements.at(0).text(), cache.people[0].movie);
25 | assert.equal(elements.at(1).text(), cache.people[0].name);
26 | done();
27 | }, 60);
28 | });
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/test/test_redux_falcor.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import assert from 'assert';
3 | import { shallow, mount } from 'enzyme';
4 | import { mock, spy, stub } from 'sinon';
5 | import { reduxFalcor } from '../src/index';
6 | import App from './src/app';
7 |
8 | function noop() {}
9 |
10 | describe('reduxFalcor', function() {
11 | describe('initialization', function() {
12 | it('throws if no wrappable component', function() {
13 | assert.throws(() => reduxFalcor());
14 | });
15 |
16 | it('wraps component', function() {
17 | const wrapper = reduxFalcor(App);
18 | assert.ok(wrapper);
19 | assert.equal(wrapper.displayName, 'ReduxFalcor(App)');
20 | assert.equal(wrapper.WrappedComponent, App);
21 | });
22 | });
23 |
24 | describe('shallow render', function() {
25 | let wrapper;
26 |
27 | before(function() {
28 | wrapper = reduxFalcor(App);
29 | });
30 |
31 | it('reduxFalcor element is created', function() {
32 | const element = React.createElement(wrapper);
33 | assert.ok(element);
34 | });
35 |
36 | it('reduxFalcor element requires store', function() {
37 | const element = React.createElement(wrapper);
38 | assert.throws(() => shallow(element), /Invariant Violation:/);
39 | });
40 |
41 | it('reduxFalcor warns when no context', function() {
42 | console.error = spy();
43 | const element = React.createElement(wrapper, { falcorStore: {}, falcor: {} });
44 | const node = shallow(element);
45 | assert.ok(console.error.calledTwice);
46 | console.error = noop;
47 | });
48 |
49 | it('reduxFalcor element is rendered', function() {
50 | const element = React.createElement(wrapper, { falcorStore: {}, falcor: {} });
51 | const node = shallow(element);
52 | assert.ok(node.some('App'));
53 | });
54 |
55 | it('reduxFalcor element has falcor prop', function() {
56 | const falcor = {};
57 | const element = React.createElement(wrapper, { falcorStore: {}, falcor });
58 | const node = shallow(element);
59 | const appNode = node.find('App').first();
60 | assert.equal(appNode.props().falcor, falcor);
61 | });
62 | });
63 |
64 | describe('mount', function() {
65 | let wrapper;
66 | let falcorStore;
67 |
68 | before(function() {
69 | wrapper = reduxFalcor(App);
70 | falcorStore = {
71 | subscribe() {}
72 | };
73 | });
74 |
75 | it('reduxFalcor is mounted', function() {
76 | const element = React.createElement(wrapper, { falcorStore, falcor: {} });
77 | const node = mount(element);
78 | assert.ok(node);
79 | });
80 |
81 | it('reduxFalcor subscribes to the store on mounting', function() {
82 | const falcorStoreMock = mock(falcorStore);
83 | falcorStoreMock.expects('subscribe').once();
84 | const element = React.createElement(wrapper, { falcorStore, falcor: {} });
85 | const node = mount(element);
86 | falcorStoreMock.verify();
87 | falcorStoreMock.restore();
88 | });
89 |
90 | it('reduxFalcor calls fetchFalcorDeps on mounting', function() {
91 | const appMock = mock(App.prototype);
92 | appMock.expects('fetchFalcorDeps').returns(Promise.resolve()).once();
93 | const falcorStoreStub = stub(falcorStore, 'subscribe').returns(noop);
94 | const element = React.createElement(wrapper, { falcorStore, falcor: {} });
95 | const node = mount(element);
96 | appMock.verify();
97 | appMock.restore();
98 | falcorStoreStub.restore();
99 | });
100 |
101 | it('reduxFalcor unsubscribes on unmounting', function() {
102 | const unsubscribe = spy();
103 | const falcorStoreStub = stub(falcorStore, 'subscribe').returns(unsubscribe);
104 | const element = React.createElement(wrapper, { falcorStore, falcor: {} });
105 | const node = mount(element);
106 | // unmount causes an exception, see Enzyme open issues.
107 | assert.throws(() => node.unmount());
108 | assert.ok(unsubscribe.calledOnce);
109 | falcorStoreStub.restore();
110 | });
111 | });
112 | });
113 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var webpack = require('webpack');
4 |
5 | var reactExternal = {
6 | root: 'React',
7 | commonjs2: 'react',
8 | commonjs: 'react',
9 | amd: 'react'
10 | }
11 |
12 | module.exports = {
13 | externals: {
14 | 'react': reactExternal
15 | },
16 | module: {
17 | loaders: [
18 | { test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ }
19 | ]
20 | },
21 | output: {
22 | library: 'ReduxFalcor',
23 | libraryTarget: 'umd'
24 | },
25 | resolve: {
26 | extensions: ['', '.js']
27 | },
28 | plugins: [
29 | new webpack.optimize.OccurenceOrderPlugin()
30 | ]
31 | };
32 |
--------------------------------------------------------------------------------