Skip to content

Commit

Permalink
Merge pull request #16 from GetAmbassador/135643565_add_ability_to_pa…
Browse files Browse the repository at this point in the history
…ss_additional_data_with_fetch_response

[#135643565] Add ability to pass additional data with fetch, create and delete response
  • Loading branch information
Ivan Sugerman authored Dec 7, 2016
2 parents 8f66b12 + a67a151 commit ea9de55
Show file tree
Hide file tree
Showing 12 changed files with 447 additions and 55 deletions.
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Redux Clerk handles the async CRUD in your Redux App.
// The tidy, minimal state managed by Redux Clerk.
{
// Full data objects are only store stored once and never duplicated.
// Full data objects are only stored once and never duplicated.
raw: {
123: { uid: 123, name: 'Apple' },
234: { uid: 234, name: 'Banana' },
Expand Down Expand Up @@ -149,9 +149,6 @@ export default todosReducer

### Selectors

#### Provided Selectors
* dataset

```
import { selectors } from 'redux-clerk'
Expand All @@ -164,6 +161,22 @@ const todosSelectors = selectors({
export default todosSelectors
```

#### Provided Selectors

##### dataset(state, datasetKey)
The dataset selector recomputes the derived data for the specified dataset.

###### state
The Redux state provided in `mapStateToProps`

Type: `object`

###### datasetKey
The name of the dataset that should be used for computing the derived data.

Type: `string` _(must be A-Za-z_0-9)_
Required: yes

## Extending an existing reducer
If you need to handle additional updates to the raw/instance data it is possible to extend the provided reducer with [reduce-reducers](https://www.npmjs.com/package/reduce-reducers).

Expand Down
2 changes: 1 addition & 1 deletion example/src/containers/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ App.propTypes = {
}

const mapStateToProps = state => ({
todos: TodoSelectors.dataset(state, 'todos')
todos: TodoSelectors.dataset(state, 'todos').get('data')
})

const mapDispatchToProps = dispatch => ({
Expand Down
8 changes: 7 additions & 1 deletion src/actions/BaseAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,23 @@ class BaseAction {
* @param {Function} dispatch - The dispatch function provided by Redux.
* @param {Object} actionData - Data to the original action.
* @param {Object} responseData - Data from the async request handlers (creator, deleter, updater, fetcher).
* @param {Object} additionalData - Optional additional data to be saved with the instance
*
* @returns {void}
*/
_dispatch = (type, dispatch, actionData, responseData) => {
_dispatch = (type, dispatch, actionData, responseData, additionalData) => {
const action = Object.assign({}, { type: this.actionNames[type] }, actionData)

// Include response data from async handler if provided
if(responseData) {
action.responseData = responseData
}

// Include additional data from async handler if provided
if(additionalData) {
action.additionalData = additionalData
}

dispatch(action)
}

Expand Down
1 change: 0 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export actions from './actions'
export reducer from './reducers'
export selectors from './selectors'
export addSubreducers from './utils/addSubreducers'
15 changes: 15 additions & 0 deletions src/reducers/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export const start = (state, action) => {
// Add uid to provided instance
const instanceData = map.getIn(['instances', action.instance, 'data']) || Immutable.fromJS([])
map.setIn(['instances', action.instance, 'data'], instanceData.insert(0, uid))

// Add additional data if provided
if(action.additionalData) {
map.mergeIn(['instances', action.instance, 'additionalData'], Immutable.fromJS(action.additionalData))
}
})
}

Expand Down Expand Up @@ -55,6 +60,11 @@ export const success = (state, action) => {
// Remove temporary uid from instance array
const temporaryUidIndex = map.getIn(['instances', action.instance, 'data']).findIndex(uid => uid === temporaryUid)
map.removeIn(['instances', action.instance, 'data', temporaryUidIndex])

// Add additional data if provided
if(action.additionalData) {
map.mergeIn(['instances', action.instance, 'additionalData'], Immutable.fromJS(action.additionalData))
}
})
}

Expand All @@ -77,6 +87,11 @@ export const error = (state, action) => {
const temporaryUid = action.record.get(action.uidField)
const temporaryUidIndex = map.getIn(['instances', action.instance, 'data']).findIndex(uid => uid === temporaryUid)
map.removeIn(['instances', action.instance, 'data', temporaryUidIndex])

// Add additional data if provided
if(action.additionalData) {
map.mergeIn(['instances', action.instance, 'additionalData'], Immutable.fromJS(action.additionalData))
}
})
}

Expand Down
24 changes: 21 additions & 3 deletions src/reducers/delete.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Map } from 'immutable'
import Immutable, { Map } from 'immutable'

/**
* The start action for the delete reducer
Expand All @@ -23,6 +23,11 @@ export const start = (state, action) => {

// Saving the item being deleted in case deletion fails
map.set('pendingDelete', state.get('pendingDelete').merge(itemPendingDeleteTuple))

// Add additional data if provided
if(action.additionalData) {
map.mergeIn(['instances', action.instance, 'additionalData'], Immutable.fromJS(action.additionalData))
}
})
}

Expand All @@ -34,8 +39,16 @@ export const start = (state, action) => {
* @return {Immutable.Map} - updated state
*/
export const success = (state, action) => {
// Remove the item pending delete
return state.deleteIn(['pendingDelete', action.uid])
return state.withMutations(map => {

// Remove the item pending delete
map.deleteIn(['pendingDelete', action.uid])

// Add additional data if provided
if(action.additionalData) {
map.mergeIn(['instances', action.instance, 'additionalData'], Immutable.fromJS(action.additionalData))
}
})
}

/**
Expand All @@ -58,6 +71,11 @@ export const error = (state, action) => {
// Re-add the deleted items
map.set('raw', state.get('raw').merge(deletedRecordTuple))
map.setIn(['instances', action.instance, 'data'], map.getIn(['instances', action.instance, 'data']).insert(deletedRecord.get('index'), action.uid))

// Add additional data if provided
if(action.additionalData) {
map.mergeIn(['instances', action.instance, 'additionalData'], Immutable.fromJS(action.additionalData))
}
})
}

Expand Down
30 changes: 22 additions & 8 deletions src/reducers/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ import Immutable from 'immutable'
/**
* The start action for the fetch reducer
* @param {Immutable.Map} state - current reducer state
* @param {Object} action - action object
*
* @return {Immutable.Map} - updated state
*/
export const start = (state) => {
// Currently we do nothing on start
// Eventually we'll set a loading state to true
return state
export const start = (state, action) => {
return state.withMutations(map => {

// Add additional data if provided
if(action.additionalData) {
map.mergeIn(['instances', action.instance, 'additionalData'], Immutable.fromJS(action.additionalData))
}
})
}

/**
Expand Down Expand Up @@ -42,21 +47,30 @@ export const success = (state, action) => {
else {
map.setIn(['instances', action.instance, 'data'], instanceData)
}
}

// Add additional data if provided
if(action.additionalData) {
map.mergeIn(['instances', action.instance, 'additionalData'], Immutable.fromJS(action.additionalData))
}
})
}

/**
* The error action for the fetch reducer
* @param {Immutable.Map} state - current reducer state
* @param {Object} action - action object
*
* @return {Immutable.Map} - updated state
*/
export const error = (state) => {
// Currently we do nothing on error
// Eventually we'll set a loading state to false
return state
export const error = (state, action) => {
return state.withMutations(map => {

// Add additional data if provided
if(action.additionalData) {
map.mergeIn(['instances', action.instance, 'additionalData'], Immutable.fromJS(action.additionalData))
}
})
}

export default {
Expand Down
11 changes: 8 additions & 3 deletions src/selectors/dataset.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Immutable from 'immutable'
import { Map, List } from 'immutable'

/**
* The selector for computing derived datasets
Expand All @@ -19,9 +19,14 @@ export const datasetSelector = (config, state, instance) => {

// If instanceData is not created yet, return empty List
if(!instanceData) {
return Immutable.fromJS([])
return List([])
}

// Re-compute data
return instanceData.map(i => rawData.get(i))
const computedData = instanceData.map(i => rawData.get(i))

// Gather additional data
const additionalData = baseState.getIn(['instances', instance, 'additionalData']) || Map({})

return additionalData.merge(Map({ data: computedData }))
}
91 changes: 84 additions & 7 deletions test/reducers/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@ describe('Reducers::Create', () => {
const previousState = Immutable.fromJS({
raw: Map([[123, Immutable.fromJS({ uid: 123, test: '123' })]]),
instances: {
test1: { data: [123] }
test1: {
data: [123],
additionalData: {
totalCount: 1
}
}
}
})

const action = {
record: Immutable.fromJS({ uid: 234, test: '234' }),
instance: 'test1',
uidField: 'uid'
uidField: 'uid',
additionalData: {
totalCount: 2
}
}

const expectedResult = {
Expand All @@ -24,7 +32,12 @@ describe('Reducers::Create', () => {
234: { uid: 234, test: '234' }
},
instances: {
test1: { data: [234, 123] }
test1: {
data: [234, 123],
additionalData: {
totalCount: 2
}
}
}
}

Expand Down Expand Up @@ -56,16 +69,20 @@ describe('Reducers::Create', () => {
})

describe('success', () => {
it('should replace the temporary uid with the permanent uid', () => {
const previousState = Immutable.fromJS({
let previousState

beforeEach(() => {
previousState = Immutable.fromJS({
raw: Map([['temp123', Immutable.fromJS({ uid: 'temp123', test: '123' })], [234, Immutable.fromJS({ uid: 234, test: '234' })]]),
instances: {
test1: {
data: ['temp123', 234]
}
}
})
})

it('should replace the temporary uid with the permanent uid', () => {
const action = {
record: Immutable.fromJS({ uid: 'temp123', test: '123' }),
responseData: { uid: 123, test: '123' },
Expand All @@ -87,19 +104,52 @@ describe('Reducers::Create', () => {

expect(success(previousState, action).toJS()).to.deep.equal(expectedResult)
})

it('should set additional data in state if provided', () => {
const action = {
record: Immutable.fromJS({ uid: 'temp123', test: '123' }),
responseData: { uid: 123, test: '123' },
instance: 'test1',
uidField: 'uid',
additionalData: {
totalCount: 2
}
}

const expectedResult = {
raw: {
123: { uid: 123, test: '123' },
234: { uid: 234, test: '234' }
},
instances: {
test1: {
data: [123, 234],
additionalData: {
totalCount: 2
}
}
}
}

expect(success(previousState, action).toJS()).to.deep.equal(expectedResult)
})
})

describe('error', () => {
it('should remove the created item', () => {
const previousState = Immutable.fromJS({
let previousState

beforeEach(() => {
previousState = Immutable.fromJS({
raw: Map([['temp123', Immutable.fromJS({ uid: 'temp123', test: '123' })], [234, Immutable.fromJS({ uid: 234, test: '234' })]]),
instances: {
test1: {
data: ['temp123', 234]
}
}
})
})

it('should remove the created item', () => {
const action = {
record: Immutable.fromJS({ uid: 'temp123', test: '123' }),
instance: 'test1',
Expand All @@ -119,5 +169,32 @@ describe('Reducers::Create', () => {

expect(error(previousState, action).toJS()).to.deep.equal(expectedResult)
})

it('should set additional data in state if provided', () => {
const action = {
record: Immutable.fromJS({ uid: 'temp123', test: '123' }),
instance: 'test1',
uidField: 'uid',
additionalData: {
totalCount: 1
}
}

const expectedResult = {
raw: {
234: { uid: 234, test: '234' }
},
instances: {
test1: {
data: [234],
additionalData: {
totalCount: 1
}
}
}
}

expect(error(previousState, action).toJS()).to.deep.equal(expectedResult)
})
})
})
Loading

0 comments on commit ea9de55

Please sign in to comment.