Skip to content

Instantly share code, notes, and snippets.

@lancegliser
Created October 4, 2017 23:32
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 lancegliser/108e9ac9c430ec54a1e33ecdd9b9e2e8 to your computer and use it in GitHub Desktop.
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
/* 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