Skip to content

Commit

Permalink
feat(cmf): add two state static accessors (#1371)
Browse files Browse the repository at this point in the history
Expose two static functions on every cmfConnected components:

function getState(reduxState, id='default')
function setStateAction(componentState, id='default', type='ComponentName.setState')
  • Loading branch information
jmfrancois committed May 28, 2018
1 parent 02a75f5 commit 75f4211
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 5 deletions.
9 changes: 5 additions & 4 deletions output/cmf.eslint.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
The react/require-extension rule is deprecated. Please use the import/extensions rule from eslint-plugin-import instead.

/home/travis/build/Talend/ui/packages/cmf/src/cmfConnect.js
218:11 error 'displayName' is not defined no-undef
219:11 error 'propTypes' is not defined no-undef
223:11 error 'contextTypes' is not defined no-undef
234:11 error 'displayName' is not defined no-undef
235:11 error 'propTypes' is not defined no-undef
239:11 error 'contextTypes' is not defined no-undef
246:11 error 'setStateAction' is not defined no-undef

/home/travis/build/Talend/ui/packages/cmf/src/componentState.js
87:3 warning Unexpected console statement no-console
Expand All @@ -26,5 +27,5 @@ The react/require-extension rule is deprecated. Please use the import/extensions
/home/travis/build/Talend/ui/packages/cmf/src/sagas/collection.js
10:1 error Prefer default export import/prefer-default-export

12 problems (9 errors, 3 warnings)
13 problems (10 errors, 3 warnings)

62 changes: 62 additions & 0 deletions packages/cmf/__tests__/cmfConnect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ describe('cmfConnect', () => {
onClick: PropTypes.func,
label: PropTypes.string,
};
Button.displayName = 'Button';
const CMFConnectedButton = cmfConnect({})(Button);
it('should create a connected component', () => {
const TestComponent = jest.fn();
Expand All @@ -211,6 +212,67 @@ describe('cmfConnect', () => {
expect(wrapper.props()).toMatchSnapshot();
});

it('should expose getState static function to get the state', () => {
expect(typeof CMFConnectedButton.getState).toBe('function');
const state = mock.state();
state.cmf.components = fromJS({
Button: {
default: { foo: 'bar' },
other: { foo: 'baz' },
},
});
expect(CMFConnectedButton.getState(state).get('foo')).toBe('bar');
expect(CMFConnectedButton.getState(state, 'other').get('foo')).toBe('baz');
});
it('should expose setStateAction static function to get the redux action to setState', () => {
expect(typeof CMFConnectedButton.setStateAction).toBe('function');
const state = new Map({ foo: 'bar' });
let action = CMFConnectedButton.setStateAction(state);
expect(action).toEqual({
type: 'Button.setState',
cmf: {
componentState: {
componentName: 'Button',
componentState: state,
key: 'default',
type: 'REACT_CMF.COMPONENT_MERGE_STATE',
},
},
});
action = CMFConnectedButton.setStateAction(state, 'foo', 'MY_ACTION');
expect(action.type).toBe('MY_ACTION');
expect(action.cmf.componentState.key).toBe('foo');
});

it('should expose setStateAction static function to get the redux action to setState', () => {
expect(typeof CMFConnectedButton.setStateAction).toBe('function');
const state = mock.state();
state.cmf.components = fromJS({
Button: {
default: { foo: 'foo' },
other: { foo: 'baz' },
},
});
let actionCreator = CMFConnectedButton.setStateAction(prevState => prevState.set('foo', 'bar'));
expect(typeof actionCreator).toBe('function');
let action = actionCreator(null, () => state);
expect(action).toMatchObject({
type: 'Button.setState',
cmf: {
componentState: {
componentName: 'Button',
key: 'default',
type: 'REACT_CMF.COMPONENT_MERGE_STATE',
},
},
});
expect(action.cmf.componentState.componentState.get('foo')).toBe('bar');
actionCreator = CMFConnectedButton.setStateAction(prevState => prevState.set('foo', 'baz'), 'other', 'MY_ACTION');
action = actionCreator(null, () => state);
expect(action.type).toBe('MY_ACTION');
expect(action.cmf.componentState.key).toBe('other');
expect(action.cmf.componentState.componentState.get('foo')).toBe('baz');
});
it('should support no context in dispatchActionCreator', () => {
const TestComponent = props => <div className="test-component" {...props} />;
TestComponent.displayName = 'TestComponent';
Expand Down
24 changes: 24 additions & 0 deletions packages/cmf/src/cmfConnect.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import hoistStatics from 'hoist-non-react-statics';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import omit from 'lodash/omit';
import actions from './actions';
import actionCreator from './actionCreator';
import component from './component';
import CONST from './constant';
Expand Down Expand Up @@ -214,6 +215,21 @@ export default function cmfConnect({
if (!WrappedComponent.displayName) {
invariant(true, `${WrappedComponent.name} has no displayName`);
}
function getState(state, id = 'default') {
return state.cmf.components.getIn([getComponentName(WrappedComponent), id], defaultState);
}
function getSetStateAction(state, id, type) {
return {
type: type || `${getComponentName(WrappedComponent)}.setState`,
cmf: {
componentState: actions.components.mergeState(
getComponentName(WrappedComponent),
id,
state,
),
},
};
}
class CMFContainer extends React.Component {
static displayName = `CMF(${getComponentName(WrappedComponent)})`;
static propTypes = {
Expand All @@ -226,6 +242,14 @@ export default function cmfConnect({
router: PropTypes.object,
};
static WrappedComponent = WrappedComponent;
static getState = getState;
static setStateAction = function setStateAction(state, id = 'default', type) {
if (typeof state !== 'function') {
return getSetStateAction(state, id, type);
}
return (_, getReduxState) =>
getSetStateAction(state(getState(getReduxState(), id)), id, type);
};

constructor(props, context) {
super(props, context);
Expand Down
39 changes: 39 additions & 0 deletions packages/cmf/src/cmfConnect.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,45 @@ If you want to render some component conditionally, just pass "renderIf" prop (t
You can also use Expression for this and customize this prop like "renderIfExpression" in
CMF json configuration files

## How to read and update component state from the outside

Every cmfConnected component expose two static functions:
* getState
* setStateAction

So if we take back the `Clock` example from below and we try to write a saga:

```javascript
import Clock from './Clock.connect';

export default function* myDeLorean() {
const clockState = yield select(Clock.getState);
const action = Clock.setStateAction(clockState.set('date', new Date('2025/12/25')));
yield put(action);
}
```

If you have multiple instance of the same component those api support `id` as a second argument.

```javascript
const componentState = Clock.getState(state, 'a-component-id');
// mutation
Clock.setStateAction(componentState, 'a-component-id');
```

If your setState rely on the previous state value and you have some async operations between you can still rely on the callback function:

```javascript
Clock.setStateAction(
prevState => prevState.set(
'minutes',
prevState.get('date').getMinutes()
),
'a-component-id'
);
```


## How to test


Expand Down
2 changes: 1 addition & 1 deletion packages/cmf/src/componentState.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class MyComponent extends React.Component {
export default cmfConnect({})(MyComponent);
*/

export function getStateProps(state, name, id) {
export function getStateProps(state, name, id = 'default') {
return {
state: state.cmf.components.getIn([name, id]),
};
Expand Down

0 comments on commit 75f4211

Please sign in to comment.