Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cmf): add two state static accessors #1371

Merged
merged 18 commits into from
May 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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