Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
S
scratch-www
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Analytics
Analytics
Repository
Value Stream
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Commits
Open sidebar
xpstem
scratch-www
Commits
6b87429e
Commit
6b87429e
authored
Mar 16, 2021
by
Paul Kaplan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Create a redux module for infinitely loading editable lists
parent
560379f9
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
302 additions
and
0 deletions
+302
-0
src/redux/infinite-list.js
src/redux/infinite-list.js
+127
-0
test/unit/redux/infinite-list.test.js
test/unit/redux/infinite-list.test.js
+175
-0
No files found.
src/redux/infinite-list.js
0 → 100644
View file @
6b87429e
/**
* @typedef ReduxModule
* A redux "module" for reusable functionality. The module exports
* a reducer function, a set of action creators and a selector
* that are all scoped to the given "key". This allows us to reuse
* this reducer multiple times in the same redux store.
*
* @property {string} key The key to use when registering this
* modules reducer in the redux state tree.
* @property {function} selector Function called with the full
* state tree to select only this modules slice of the state.
* @property {object} actions An object of action creator functions
* to call to make changes to the data in this reducer.
* @property {function} reducer A redux reducer that takes an action
* from the action creators and the current state and returns
* the next state.
*/
/**
* @typedef {function} InfiniteListFetcher
* A function to call that returns more data for the InfiniteList
* loadMore action. It must resolve to {items: [], moreToLoad} or
* reject with the error {statusCode}.
* @returns {Promise<{items:[], moreToLoad:boolean}>}
*/
/**
* A redux module to create a list of items where more items can be loaded
* using an API. Additionally, there are actions for prepending items
* to the list, removing items and handling load errors.
*
* @param {string} key - used to scope action names and the selector
* This key must be unique among other instances of this module.
* @returns {ReduxModule} the redux module
*/
const
InfiniteList
=
key
=>
{
const
initialState
=
{
items
:
[],
offset
:
0
,
error
:
null
,
loading
:
true
,
moreToLoad
:
false
};
const
reducer
=
(
state
,
action
)
=>
{
if
(
typeof
state
===
'
undefined
'
)
{
state
=
initialState
;
}
switch
(
action
.
type
)
{
case
`
${
key
}
_LOADING`
:
return
{
...
state
,
error
:
null
,
loading
:
true
};
case
`
${
key
}
_APPEND`
:
return
{
...
state
,
items
:
state
.
items
.
concat
(
action
.
items
),
loading
:
false
,
error
:
null
,
moreToLoad
:
action
.
moreToLoad
};
case
`
${
key
}
_REPLACE`
:
return
{
...
state
,
items
:
state
.
items
.
map
((
item
,
i
)
=>
{
if
(
i
===
action
.
index
)
return
action
.
item
;
return
item
;
})
};
case
`
${
key
}
_REMOVE`
:
return
{
...
state
,
items
:
state
.
items
.
filter
((
_
,
i
)
=>
i
!==
action
.
index
)
};
case
`
${
key
}
_PREPEND`
:
return
{
...
state
,
items
:
[
action
.
item
].
concat
(
state
.
items
)
};
case
`
${
key
}
_ERROR`
:
return
{
...
state
,
error
:
action
.
error
,
loading
:
false
,
moreToLoad
:
false
};
default
:
return
state
;
}
};
const
actions
=
{
create
:
item
=>
({
type
:
`
${
key
}
_PREPEND`
,
item
}),
remove
:
index
=>
({
type
:
`
${
key
}
_REMOVE`
,
index
}),
replace
:
(
index
,
item
)
=>
({
type
:
`
${
key
}
_REPLACE`
,
index
,
item
}),
error
:
error
=>
({
type
:
`
${
key
}
_ERROR`
,
error
}),
loading
:
()
=>
({
type
:
`
${
key
}
_LOADING`
}),
append
:
(
items
,
moreToLoad
)
=>
({
type
:
`
${
key
}
_APPEND`
,
items
,
moreToLoad
}),
/**
* Load more action returns a thunk. It takes a function to call to get more items.
* It will call the LOADING action before calling the fetcher, and call
* APPEND with the results or call ERROR.
* @param {InfiniteListFetcher} fetcher - function that returns a promise
* which must resolve to {items: [], moreToLoad}.
* @returns {function} a thunk that sequences the load and dispatches
*/
loadMore
:
fetcher
=>
(
dispatch
=>
{
dispatch
(
actions
.
loading
());
return
fetcher
()
.
then
(({
items
,
moreToLoad
})
=>
dispatch
(
actions
.
append
(
items
,
moreToLoad
)))
.
catch
(
error
=>
dispatch
(
actions
.
error
(
error
)));
})
};
const
selector
=
state
=>
state
[
key
];
return
{
key
,
actions
,
reducer
,
selector
};
};
export
default
InfiniteList
;
test/unit/redux/infinite-list.test.js
0 → 100644
View file @
6b87429e
/* global Promise */
import
InfiniteList
from
'
../../../src/redux/infinite-list
'
;
const
module
=
InfiniteList
(
'
test-key
'
);
let
initialState
;
describe
(
'
Infinite List redux module
'
,
()
=>
{
beforeEach
(()
=>
{
initialState
=
module
.
reducer
(
undefined
,
{});
// eslint-disable-line no-undefined
});
describe
(
'
reducer
'
,
()
=>
{
test
(
'
module contains a reducer
'
,
()
=>
{
expect
(
typeof
module
.
reducer
).
toBe
(
'
function
'
);
});
test
(
'
initial state
'
,
()
=>
{
expect
(
initialState
).
toMatchObject
({
loading
:
true
,
error
:
null
,
items
:
[],
moreToLoad
:
false
});
});
describe
(
'
LOADING
'
,
()
=>
{
let
action
;
beforeEach
(()
=>
{
action
=
module
.
actions
.
loading
();
initialState
.
loading
=
false
;
initialState
.
items
=
[
1
,
2
,
3
];
initialState
.
error
=
new
Error
();
});
test
(
'
sets the loading state
'
,
()
=>
{
const
newState
=
module
.
reducer
(
initialState
,
action
);
expect
(
newState
.
loading
).
toBe
(
true
);
});
test
(
'
maintains any existing data
'
,
()
=>
{
const
newState
=
module
.
reducer
(
initialState
,
action
);
expect
(
newState
.
items
).
toBe
(
initialState
.
items
);
});
test
(
'
clears any existing error
'
,
()
=>
{
const
newState
=
module
.
reducer
(
initialState
,
action
);
expect
(
newState
.
error
).
toBe
(
null
);
});
});
describe
(
'
APPEND
'
,
()
=>
{
let
action
;
beforeEach
(()
=>
{
action
=
module
.
actions
.
append
([
4
,
5
,
6
],
true
);
});
test
(
'
appends the new items
'
,
()
=>
{
initialState
.
items
=
[
1
,
2
,
3
];
const
newState
=
module
.
reducer
(
initialState
,
action
);
expect
(
newState
.
items
).
toEqual
([
1
,
2
,
3
,
4
,
5
,
6
]);
});
test
(
'
sets the moreToLoad state
'
,
()
=>
{
initialState
.
moreToLoad
=
false
;
const
newState
=
module
.
reducer
(
initialState
,
action
);
expect
(
newState
.
moreToLoad
).
toEqual
(
true
);
});
test
(
'
clears any existing error and loading state
'
,
()
=>
{
initialState
.
error
=
new
Error
();
initialState
.
loading
=
true
;
const
newState
=
module
.
reducer
(
initialState
,
action
);
expect
(
newState
.
error
).
toBe
(
null
);
expect
(
newState
.
error
).
toBe
(
null
);
});
});
describe
(
'
REPLACE
'
,
()
=>
{
let
action
;
beforeEach
(()
=>
{
action
=
module
.
actions
.
replace
(
2
,
55
);
});
test
(
'
replaces the given index with the new item
'
,
()
=>
{
initialState
.
items
=
[
8
,
9
,
10
,
11
];
const
newState
=
module
.
reducer
(
initialState
,
action
);
expect
(
newState
.
items
).
toEqual
([
8
,
9
,
55
,
11
]);
});
});
describe
(
'
REMOVE
'
,
()
=>
{
let
action
;
beforeEach
(()
=>
{
action
=
module
.
actions
.
remove
(
2
);
});
test
(
'
removes the given index
'
,
()
=>
{
initialState
.
items
=
[
8
,
9
,
10
,
11
];
const
newState
=
module
.
reducer
(
initialState
,
action
);
expect
(
newState
.
items
).
toEqual
([
8
,
9
,
11
]);
});
});
describe
(
'
CREATE
'
,
()
=>
{
let
action
;
beforeEach
(()
=>
{
action
=
module
.
actions
.
create
(
7
);
});
test
(
'
prepends the given item
'
,
()
=>
{
initialState
.
items
=
[
8
,
9
,
10
,
11
];
const
newState
=
module
.
reducer
(
initialState
,
action
);
expect
(
newState
.
items
).
toEqual
([
7
,
8
,
9
,
10
,
11
]);
});
});
describe
(
'
ERROR
'
,
()
=>
{
let
action
;
let
error
=
new
Error
();
beforeEach
(()
=>
{
action
=
module
.
actions
.
error
(
error
);
});
test
(
'
sets the error state
'
,
()
=>
{
const
newState
=
module
.
reducer
(
initialState
,
action
);
expect
(
newState
.
error
).
toBe
(
error
);
});
test
(
'
resets loading to false
'
,
()
=>
{
initialState
.
loading
=
true
;
const
newState
=
module
.
reducer
(
initialState
,
action
);
expect
(
newState
.
loading
).
toBe
(
false
);
});
test
(
'
maintains any existing data
'
,
()
=>
{
initialState
.
items
=
[
1
,
2
,
3
];
const
newState
=
module
.
reducer
(
initialState
,
action
);
expect
(
newState
.
items
).
toEqual
([
1
,
2
,
3
]);
});
});
});
describe
(
'
action creators
'
,
()
=>
{
test
(
'
module contains actions creators
'
,
()
=>
{
// The actual action creators are tested above in the reducer tests
for
(
let
key
in
module
.
actions
)
{
expect
(
typeof
module
.
actions
[
key
]).
toBe
(
'
function
'
);
}
});
describe
(
'
loadMore
'
,
()
=>
{
test
(
'
returns a thunk function, rather than a standard action object
'
,
()
=>
{
expect
(
typeof
module
.
actions
.
loadMore
()).
toBe
(
'
function
'
);
});
test
(
'
calls loading and the fetcher
'
,
()
=>
{
let
dispatch
=
jest
.
fn
();
let
fetcher
=
jest
.
fn
(()
=>
new
Promise
(()
=>
{
}));
// that never resolves
module
.
actions
.
loadMore
(
fetcher
)(
dispatch
);
expect
(
dispatch
).
toHaveBeenCalledWith
(
module
.
actions
.
loading
());
expect
(
fetcher
).
toHaveBeenCalled
();
});
test
(
'
calls append with resolved result from fetcher
'
,
async
()
=>
{
let
dispatch
=
jest
.
fn
();
let
fetcher
=
jest
.
fn
(()
=>
Promise
.
resolve
({
items
:
[
'
a
'
,
'
b
'
],
moreToLoad
:
false
}));
await
module
.
actions
.
loadMore
(
fetcher
)(
dispatch
);
expect
(
dispatch
.
mock
.
calls
[
1
][
0
])
// the second call to dispatch, after LOADING
.
toEqual
(
module
.
actions
.
append
([
'
a
'
,
'
b
'
],
false
));
});
test
(
'
calls error with rejecting promise from fetcher
'
,
async
()
=>
{
let
error
=
new
Error
();
let
dispatch
=
jest
.
fn
();
let
fetcher
=
jest
.
fn
(()
=>
Promise
.
reject
(
error
));
await
module
.
actions
.
loadMore
(
fetcher
)(
dispatch
);
expect
(
dispatch
.
mock
.
calls
[
1
][
0
])
// the second call to dispatch, after LOADING
.
toEqual
(
module
.
actions
.
error
(
error
));
});
});
});
describe
(
'
selector
'
,
()
=>
{
test
(
'
will return the slice of state defined by the key
'
,
()
=>
{
const
state
=
{
[
module
.
key
]:
module
.
reducer
(
undefined
,
{})
// eslint-disable-line no-undefined
};
expect
(
module
.
selector
(
state
)).
toBe
(
initialState
);
});
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment