Last active
November 17, 2015 19:48
-
-
Save cesarandreu/8b1c5c6efe9c649b425b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { applyMiddleware } from 'redux' | |
/** | |
* Default browser search class implementation | |
* This is in order to allow being replaced for testing and server-side rendering | |
* Since you need to communicate with web workers, this is best modeled as an observable | |
* I don't think onComplete makes sense for something like search, so it can probably be ignored | |
* Anyway, I really suggest requiring it implement the observable interface: | |
* { subscribe(onNext, onError, onComplete) => { dispose() } } | |
* Dunno about the rest of the API | |
*/ | |
import SearchClass from './Search' | |
// Default state selector | |
export function defaultSearchStateSelector (state) { | |
return state.search | |
} | |
// Constants | |
export const SEARCH_API = '@@reduxSearch/API' | |
export const SEARCH_ACTION = '@@reduxSearch/action' | |
export const SEARCH_ACTION_SEARCH = '@@reduxSearch/actions/search' | |
export const SEARCH_ACTION_RECEIVE_RESULT = '@@reduxSearch/receiveResult' | |
export const SEARCH_STATE_SELECTOR = '@@reduxSearch/searchStateSelector' | |
// Action creators | |
export function searchAPI (method) { | |
return (...args) => ({ | |
type: SEARCH_API, | |
payload: { | |
method, | |
args | |
} | |
}) | |
} | |
export const defineIndex = searchAPI('defineIndex') | |
export const indexResource = searchAPI('indexResource') | |
export const performSearch = searchAPI('performSearch') | |
export function search (resource, text) { | |
return { | |
type: SEARCH_ACTION, | |
payload: { | |
type: SEARCH_ACTION_SEARCH, | |
resource, | |
text | |
} | |
} | |
} | |
export function receiveResult (resource, result, text) { | |
return { | |
type: SEARCH_ACTION_RECEIVE_RESULT, | |
payload: { | |
resource, | |
result, | |
text | |
} | |
} | |
} | |
// Reducer | |
// User is responsible for adding this to their reducer tree | |
export function searchStateReducer (state = { resources: {} }, action) { | |
const { payload, type } = action | |
switch (type) { | |
case SEARCH_ACTION_RECEIVE_RESULT: | |
const currentResource = state.resources[payload.resource] | |
// Only accept the result if the received text matches the stored text | |
if (currentResource.text !== payload.text) { | |
return state | |
} | |
return { | |
resources: { | |
...state.resources, | |
[payload.resource]: { | |
isSearching: false, | |
result: payload.result, | |
text: currentResource.text | |
} | |
} | |
} | |
case SEARCH_ACTION_SEARCH: | |
return { | |
resources: { | |
...state.resources, | |
[payload.resource]: { | |
isSearching: true, | |
result: [], | |
text: payload.text | |
} | |
} | |
} | |
default: | |
return state | |
} | |
} | |
/** | |
* Middleware for interacting with the search API | |
* @param {Search} Search object | |
*/ | |
export function searchMiddleware (search) { | |
return () => next => action => { | |
if (action.type === SEARCH_API) { | |
const { method, args } = action.payload | |
return search[method](...args) | |
} else { | |
return next(action) | |
} | |
} | |
} | |
/** | |
* Middleware for performing search actions | |
* @param {Search} Search object | |
*/ | |
export function searchActionMiddleware () { | |
return ({ dispatch }) => next => action => { | |
if (action.type === SEARCH_ACTION) { | |
const { payload: { type, text, resource } } = action | |
if (type === SEARCH_ACTION_SEARCH) { | |
dispatch(performSearch(resource, text)) | |
next({ type, payload: { text, resource } }) | |
} | |
} else { | |
return next(action) | |
} | |
} | |
} | |
export default function reduxSearch (options = {}) { | |
return createStore => (reducer, initialState) => { | |
const { | |
resourceSelector, | |
resourceIndexes = {}, | |
Search = SearchClass, | |
searchStateSelector = defaultSearchStateSelector | |
} = options | |
const search = new Search() | |
Object.keys(resourceIndexes).forEach(resourceName => { | |
search.defineIndex(resourceName, resourceIndexes[resourceName]) | |
}) | |
const store = applyMiddleware( | |
searchMiddleware(search), | |
searchActionMiddleware() | |
)(createStore)(reducer, initialState) | |
store.search = search | |
store[SEARCH_STATE_SELECTOR] = searchStateSelector | |
search.subscribe(({ result, resource, text }) => { | |
// Here we handle item responses | |
// It can be fancier, but at its core this is all it is | |
store.dispatch(receiveResult(resource, result, text)) | |
}, error => { | |
// Somehow handle error, idk | |
// redux-router lets you pass a callback | |
throw error | |
}) | |
// This is for auto-indexing, otherwise the user is responsible | |
// User can specify a selector that returns an object map | |
// Each iterable object is a resource | |
// { [resourceName: string]: Iterable<Object> } | |
if (resourceSelector) { | |
let currentResources | |
store.subscribe(() => { | |
const nextState = store.getState() | |
const nextResources = resourceSelector(nextState) | |
// Needless to say, this check should be more robust | |
// This is just for the sake of showing an example :D | |
// You could check each resource iterable, or even each individual resource | |
if (currentResources !== nextResources) { | |
currentResources = nextResources | |
Object.keys(currentResources).forEach(resourceName => { | |
store.dispatch(indexResource(resourceName, currentResources[resourceName])) | |
}) | |
} | |
}) | |
} | |
return store | |
} | |
} | |
export function getSelectors (searchStateSelector = defaultSearchStateSelector, resourceName) { | |
return { | |
resultSelectorFactory (resourceName) { | |
return state => { | |
const { result = [] } = searchStateSelector(state)[resourceName] | |
return result | |
} | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment