Skip to content

Instantly share code, notes, and snippets.

@cesarandreu
Last active November 17, 2015 19:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cesarandreu/8b1c5c6efe9c649b425b to your computer and use it in GitHub Desktop.
Save cesarandreu/8b1c5c6efe9c649b425b to your computer and use it in GitHub Desktop.
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