Created
October 4, 2017 23:32
-
-
Save lancegliser/108e9ac9c430ec54a1e33ecdd9b9e2e8 to your computer and use it in GitHub Desktop.
An example of using Redux, without the need for React, Vue, Angular, or even a build process
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
/* globals Redux, settings */ | |
jQuery(document).ready(function(){ | |
reduxSearch(settings); | |
}); | |
function reduxSearch(settings){ | |
/** | |
* Constants | |
*/ | |
var ACTION_CLICK = 'click'; | |
var ACTION_APPLY_ELEMENT_FILTER = 'update_fliter'; | |
var ACTION_RESET_FILTERS = 'reset_filters'; | |
var ACTION_APPLY_CONTROLS = 'apply_controls'; | |
var ACTION_CONTROLS_AJAX_STARTED = 'controls_ajax_started'; | |
var ACTION_CONTROLS_AJAX_SUCCESS = 'controls_ajax_success'; | |
var ACTION_CONTROLS_AJAX_FAILURE = 'controls_ajax_failure'; | |
var VIEW_MODE_TILES = 'tiles'; | |
var VIEW_MODE_LIST = 'list'; | |
var ACTION_APPLY_VIEW_MODE_TILES = 'tile_view'; | |
var ACTION_APPLY_VIEW_MODE_LIST = 'list_view'; | |
var previousControlSignature; | |
var previousResultsState; | |
/** | |
* {HTMLElement} search results pane | |
*/ | |
var resultsElement; | |
/** {NodeList} */ | |
var filterElements; | |
var store; | |
// You may use this format when creating an initialState | |
// in settings to pass filtering and sorting data | |
var defaultControlsState = { | |
filters: { | |
primary: {}, | |
location_proximity: 20 | |
}, | |
sorts: {} | |
}; | |
init(); | |
function init(){ | |
// Define UI elements | |
resultsElement = document.querySelector('#search-results'); | |
// Create a redux state data store | |
// Define the multiple reducers | |
var combinedReducer = Redux.combineReducers({ | |
controls: controls, | |
viewMode: viewMode, | |
interactions: interactions, | |
ajax: ajax | |
}); | |
// Define the initial state (see defaultControlsState) | |
var initialState; | |
if( settings.initialControlsState ){ | |
initialState = { controls: Object.assign({}, settings.initialControlsState, defaultControlsState) }; | |
} | |
// Create the store | |
store = Redux.createStore(combinedReducer, initialState); | |
// Setup to repaint when the state changes | |
store.subscribe(render); | |
// Handle our first paint manually | |
render(); | |
bindEvents(); | |
applyFilters({ | |
isLocked: true, // We don't have proper filters to event display | |
isForced: true // Initial state is assured to match, but we need the results to render dynamic filters | |
}); | |
} | |
function bindEvents(){ | |
// Bind events to make things happen! | |
document.addEventListener('click', clickHandler); | |
// Rebind to any filters we may have pained onto the screen in our previous render | |
rebindFilters(); | |
} | |
/** | |
* Called multiple times to handle filters that have been introduced later | |
*/ | |
function rebindFilters(){ | |
// For setting filters into the the store, but not firing the apply action | |
filterElements = document.querySelectorAll('[data-filter]'); | |
filterElements.forEach(function(element){ | |
// We don't want to listen for apply-filter elements | |
// Those call their own update if required to ensure ordering | |
if( element.dataset.hasOwnProperty('applyFilter') ){ | |
return true; | |
} | |
if( element.filterEventBound ){ | |
return true; | |
} | |
element.addEventListener('change', handleUpdateFilters); | |
element.filterEventBound = true; | |
}); | |
// For applying the store's new filters | |
var applyFilterElements = document.querySelectorAll('[data-apply-filter]'); | |
applyFilterElements.forEach(function(element){ | |
if( element.applyEventBound ){ | |
return true; | |
} | |
element.addEventListener('change', handleApplyFiltersChange); | |
element.applyEventBound = true; | |
}); | |
} | |
/** | |
* Returns the portions of the redux store relevant to the controls | |
* @returns {object} | |
*/ | |
function getControlsState(){ | |
var state = store.getState(); | |
return { | |
controls: state.controls, | |
viewMode: state.viewMode, | |
ajax: state.ajax | |
}; | |
} | |
/** | |
* Returns the portions of the redux store relevant to the results | |
* @returns {object} | |
*/ | |
function getResultsState(){ | |
var state = store.getState(); | |
return { | |
viewMode: state.viewMode, | |
ajax: state.ajax, | |
results: undefined | |
}; | |
} | |
/** | |
* Event handlers | |
*/ | |
/** | |
* @param {Event} event | |
*/ | |
function clickHandler(event){ | |
store.dispatch({ type : ACTION_CLICK, event: event }); | |
} | |
/** | |
* @param {Event} event | |
*/ | |
function handleUpdateFilters(event){ | |
updateFilters(event); | |
} | |
/** | |
* @param {Event} event | |
*/ | |
function handleApplyFiltersChange(event){ | |
if( event.target.dataset.hasOwnProperty('filter') ){ | |
updateFilters(event); | |
} | |
applyFilters({ | |
action: ACTION_APPLY_CONTROLS | |
}); | |
} | |
/** | |
* @param {Event} event | |
*/ | |
function updateFilters(event){ | |
store.dispatch({ type : ACTION_APPLY_ELEMENT_FILTER, event: event }); | |
} | |
/** | |
* Fires an ajax call to get new filters, results, etc. | |
* It updates the state based on the state of the ajax request. | |
* | |
* @param {object} options | |
*/ | |
function applyFilters(options){ | |
options.isLocked = options.isLocked || false; | |
options.isForced = options.isForced || false; | |
var state = store.getState(); | |
var parameters = { | |
action: options.action, | |
filters: state.controls.filters, | |
sorts: state.controls.sorts | |
}; | |
store.dispatch({ type : ACTION_CONTROLS_AJAX_STARTED, | |
event: event, | |
areControlsLocked: options.isLocked | |
}); | |
jQuery.ajax(settings.ajaxUrl, { | |
method: 'POST', | |
data: parameters, | |
dataType: 'json', | |
error: _applyFiltersError, | |
success: _applyFiltersSuccess | |
}); | |
} | |
/** | |
* @param jqXHR | |
* @param {string} textStatus | |
* @param {string} errorThrown | |
*/ | |
function _applyFiltersError( jqXHR , textStatus, errorThrown){ | |
store.dispatch({ type : ACTION_CONTROLS_AJAX_FAILURE, | |
jqXHR: jqXHR, | |
textStatus: textStatus, | |
errorThrown: errorThrown | |
}); | |
} | |
/** | |
* @param data | |
*/ | |
function _applyFiltersSuccess(data){ | |
store.dispatch({ type : ACTION_CONTROLS_AJAX_SUCCESS, data: data }); | |
} | |
/** | |
* Primary 'something has changed' render again please starting point. | |
* This is the end of Redux, and the start of React, Vue, or your own special handling. | |
*/ | |
function render(){ | |
// React would normally handle this kind of differential calculation and selective rendering | |
// You really should look into that someday if you think the below is as mad as I do. | |
var controlsState = getControlsState(); | |
var controlSignature = JSON.stringify(controlsState); | |
if(previousControlSignature !== controlSignature){ | |
previousControlSignature = controlSignature; | |
renderControls(controlsState); | |
} | |
var resultsState = getResultsState(); | |
resultsElement.innerHTML = JSON.stringify(resultsState); | |
} | |
/** | |
* | |
* @param state | |
*/ | |
function renderControls(state){ | |
document.querySelector('#controls-state').innerHTML = JSON.stringify(state); | |
var filterElements = document.querySelectorAll('[data-filter]'); | |
// Locking (due to market changes and other conditions) | |
filterElements.forEach(function(element){ | |
element.disabled = state.ajax.areControlsLocked; | |
}); | |
filterElements.forEach(function(element){ | |
_updateElementByFilters(state.controls.filters, element); | |
}); | |
/** | |
* @param {object} filters | |
* @param {HTMLElement} element | |
* @private | |
*/ | |
function _updateElementByFilters(filters, element) { | |
var filterType = element.dataset.filter; | |
var property, value; | |
// Figure out where we stored it | |
switch (element.type) { | |
case 'checkbox': | |
case 'radio': | |
property = element.value; | |
value = element.checked; | |
break; | |
case 'select': | |
// I Sure did not do this for this demo | |
break; | |
default: | |
property = element.name; | |
if (!!element.value) { | |
value = element.value; | |
} | |
} | |
// Get the value out | |
switch (filterType) { | |
case '': | |
value = filters[property] || null; | |
break; | |
default: | |
value = filters[filterType][property] || null; | |
} | |
// Set it | |
// console.debug('Setting element by filter', filterType, element.name, value); | |
switch (element.type) { | |
case 'checkbox': | |
case 'radio': | |
element.checked = value; | |
break; | |
case 'select': | |
// I Sure did not do this for this demo | |
break; | |
default: | |
element.value = value; | |
} | |
return filters; | |
} | |
} | |
/** | |
* Redux state reducers | |
*/ | |
/** | |
* A composing reduce function. It's handed state controls affecting data | |
* @param state | |
* @param action | |
*/ | |
function controls(state, action){ | |
// If state is undefined, return the initial application state | |
if (typeof state === 'undefined') { | |
return Object.assign({}, defaultControlsState); | |
} | |
switch(action.type){ | |
case ACTION_CONTROLS_AJAX_SUCCESS: | |
return Object.assign({}, state, { | |
filters: _updateFiltersByMetadata(state.filters, action.data.metadata) | |
}); | |
case ACTION_APPLY_ELEMENT_FILTER: | |
return Object.assign({}, state, { | |
filters: _updateFiltersByElement(state.filters, action.event) | |
}); | |
case ACTION_RESET_FILTERS: | |
return Object.assign({}, state, { | |
filters: {} | |
}); | |
default: | |
return state; | |
} | |
/** | |
* @param {object} filters | |
* @param {object} metadata | |
* @return object | |
* @private | |
*/ | |
function _updateFiltersByMetadata(filters, metadata){ | |
// debugger; | |
return filters; | |
} | |
/** | |
* @param {object} filters | |
* @param {Event} event | |
* @return object | |
* @private | |
*/ | |
function _updateFiltersByElement(filters, event){ | |
var element = event.target; | |
var filterType = element.dataset.filter; | |
var property, value; | |
switch(element.type){ | |
case 'checkbox': | |
case 'radio': | |
property = element.value; | |
value = element.checked; | |
break; | |
case 'select': | |
// I Sure did not do this for this demo | |
break; | |
default: | |
property = element.name; | |
if( !!element.value ){ | |
value = element.value; | |
} | |
} | |
switch(filterType){ | |
case '': | |
filters[property] = value; | |
break; | |
default: | |
filters[filterType][property] = value; | |
} | |
return filters; | |
} | |
} | |
/** | |
* A composing reduce function. It's handed state.viewMode only | |
* @param viewMode | |
* @param action | |
*/ | |
function viewMode(viewMode, action){ | |
if (typeof state === 'undefined') { | |
return VIEW_MODE_TILES; | |
} | |
switch(action.type){ | |
case ACTION_APPLY_VIEW_MODE_TILES: | |
return VIEW_MODE_TILES; | |
case ACTION_APPLY_VIEW_MODE_LIST: | |
return VIEW_MODE_LIST; | |
default: | |
return viewMode; | |
} | |
} | |
/** | |
* A composing reduce function. It's handed state.interactions only | |
* @param state | |
* @param action | |
*/ | |
function interactions(state, action){ | |
if (typeof state === 'undefined') { | |
return { | |
event: undefined, | |
clicksTotal: 0 | |
}; | |
} | |
switch(action.type){ | |
case ACTION_CLICK: | |
return Object.assign({}, state, { | |
event: action.event, | |
clicksTotal: state.clicksTotal += 1 | |
}); | |
default: | |
return state; | |
} | |
} | |
/** | |
* A composing reduce function. It's handed only state.ajax | |
* @param state | |
* @param action | |
*/ | |
function ajax(state, action){ | |
if (typeof state === 'undefined') { | |
return { | |
isFetchingControls: false, | |
areControlsLocked: false, | |
isFetchingResults: false, | |
isError: false | |
}; | |
} | |
switch(action.type){ | |
case ACTION_CONTROLS_AJAX_STARTED: | |
return Object.assign({}, state, { | |
isFetchingControls: true, | |
areControlsLocked: action.areControlsLocked || false, | |
isError: false | |
}); | |
case ACTION_CONTROLS_AJAX_SUCCESS: | |
return Object.assign({}, state, { | |
isFetchingControls: false, | |
areControlsLocked: false, | |
isError: false | |
}); | |
case ACTION_CONTROLS_AJAX_FAILURE: | |
return Object.assign({}, state, { | |
isFetchingControls: false, | |
areControlsLocked: false, | |
isError: true | |
}); | |
default: | |
return state; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment