Skip to content

Instantly share code, notes, and snippets.

@a-laughlin
Created August 17, 2017 22:27
Show Gist options
  • Save a-laughlin/72e77c506c372f95e4721cf2ac30d7ae to your computer and use it in GitHub Desktop.
Save a-laughlin/72e77c506c372f95e4721cf2ac30d7ae to your computer and use it in GitHub Desktop.
User Goals HOC
// HOC designed around enabling components to consistently work with user's mental state, rather than just program state.
// storing this here for reference...
import React from 'react'; // so jsx parsing works
import {withProps,withReducer,mapProps,setPropTypes,branch} from 'recompose';
import {ifElse} from 'ramda';
import {
spread, rest, pick, isString, compose, pipe, uniq, startCase, get, map, values, flatten, sortBy,
filter, identity, keys, without, omit, transform as transformFP
} from 'lodash/fp';
// reference
// https://medium.com/@jamsesso/a-simple-react-finite-state-machine-design-pattern-9078e343bdd7
// https://www.google.com/search?num=20&q=human+action+cycle+%22state+machine%22&oq=human+action+cycle+%22state+machine%22
// organize app's components around goals user can achieve
// organize components within a goal to ensure each step of human action cycle has feedback fitting a great user experience
// track state of each goal's achievement (for both user feedback and granular data)
//
// constraints
// defines each goal as a finite state machine
// states must conform to standard goal achievement syntax for consistency
//
// design considerations
// invert control where possible for external event handler fn composition
// describes states declaratively (abstracts away component switch via statesMap object)
// accepts custom dispatch fn
// base HOC should work like any other HOC
// compose in additional states beyond view-only, for action executing, cancelling, etc
// ? normalize all dispatches as async?
// ? maybe... compose states together via Object.assign(baseStates,otherStates) ... unsure if describing them completely as strings will work
// ? how to structure state component map?
//
// assumptions
// assumes any component can contribute to 1-n goals
// assumes user can be actively attempting 1-n goals
// assumes any goal can be nested... though 1-n parent goals since child goals may or may not contribute to both... define children goals on parent.
// assumes user tracks goals as a series of directed acyclical graphs
//
// implementation considerations
// the functionality is very similar to routing, where the path reflects path in nested goals... may be able to use router for underlying state
export const UserGoalsHOC = ()=>{
const DefaultComponent = (props)=>React.createElement('div',props);
const activeGoalStates = {
activePath:[],
root:{
status:'formingGoal',
data:{},
children:{
findContent:{
status:'formingGoal',
children:{
navigate:{
status:'formingGoal',
},
search:{},
},
},
}
}
};
// state:{}
// navigate:{},
// search:{},
// goalHistory:[],
// stateTree={}
// onEntry, onExit, Component, exitHandlers:
// construct the exit handlers from each state to the possible other states
// formingGoal:{nextStateDispatchers:{evaluatingPossiblePlansVisually}}
//
const DefaultStateComponents = {
formingGoal:DefaultComponent, // content + style affordances
evaluatingPossiblePlansVisually:DefaultComponent, // IA (semantic mapping) + Design (concept mapping) + Design (affordances) + Constraints (Component does not exist)
evaluatingThisComponentAsExecutionPlan:DefaultComponent, // css affordances: hover with mouse (cursor change), tap with finger, titles, sometimes components
executingPlan:DefaultComponent // e.g., progress - like mouseDown or held tap on element
evaluatingPlanExecution:DefaultComponent // progress - feedback when
// attemptingToAchieveGoal:DefaultComponent, // css affordances: hover/click, cursor change, titles, sometimes components
// attemptingToChangeGoal:DefaultComponent, // css affordances: hover/click, cursor change, titles, sometimes components
evaluatingAttemptProgress:DefaultComponent, // component feedback
evaluatingAttemptSuccess:DefaultComponent, // component feedback
evaluatingAttemptError:DefaultComponent, // component feedback
// goalCancel:DefaultComponent, // cancel would be handled by a parent component since this component is just a way to achieve a goal, not the goal itself
// if a user switches goals, they'll use a different component
};
const DISPATCH_NAME = 'dispatch';
const dispatchers = {
formingGoal:{
evaluatingPossiblePlansVisually:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'evaluatingPossiblePlansVisually'}}),
previousState:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'previousState'}}),
},
// should entry state differs if user is using this component to achieve some other goal?
// cases - user knows what they want to do, user does not know -
// component shouldn't know if it's being used... parents should know about children states,
// children should not know bout parents, so they can be composed
// needs a way for parent to indicate cancel, for cleanup. Maybe. Might be able to do this as part of the tree mgmt.
evaluatingPossiblePlansVisually:{
evaluatingThisComponentAsExecutionPlan:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'evaluatingThisComponentAsExecutionPlan'}}),
previousState:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'previousState'}}),
},
evaluatingThisComponentAsExecutionPlan:{
executingPlan:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'executingPlan'}}),
previousState:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'previousState'}}),
},
// evaluating HOC
// executing HOC
executingPlan:{ // mousedown on download
evaluatingAttemptAction:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'evaluatingPlanExecution'}}),
previousState:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'previousState'}}),
},
evaluatingPlanExecution:{ // e.g., starting download
evaluatingPlanExecutionProgress:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'evaluatingPlanExecutionProgress'}}),
evaluatingPlanExecutionSuccess:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'evaluatingPlanExecutionSuccess'}}),
evaluatingPlanExecutionError:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'evaluatingPlanExecutionError'}}),
previousState:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'previousState'}}),
},
evaluatingPlanExecutionProgress:{
abort:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'attemptingAbort'}}),
evalSuccess:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'evaluatingPlanExecutionSuccess'}}),
evalError:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'evaluatingPlanExecutionError'}}),
evalPrevious:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'previousState'}}),
},
evaluatingPlanExecutionSuccess:{
complete:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'evaluatingPossiblePlansVisually'}}),
reAttemptGoal:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'executingPlan'}}),
reEvaluatePlan:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'evaluatingThisComponentAsExecutionPlan'}}),
},
evaluatingPlanExecutionError:{
abandon:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'evaluatingAbandonAttempt'}}),
reAttemptGoal:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'executingPlan'}}),
reEvaluatePlan:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'evaluatingThisComponentAsExecutionPlan'}}),
evalUnexpectedError:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'evaluatingThisComponentAsExecutionPlan'}}),
},
evaluatingUnexpectedError:{to:{
contact:'evaluatingUnexpectedErrorInstructions',
abandon:'evaluatingAbandonAttempt',
reEvaluatePlan:'',
reAttemptGoal:'',
}},
// abortable HOC
attemptingAbort:{to:{evalAbortAttempt:'evaluatingAbortAttempt'}},
evaluatingAbortAttempt:{to:{progress:'evaluatingAbortProgress', success:'evaluatingAbortSuccess', error:'evaluatingAbortError'}},
evaluatingAbortProgress:{to:{ success:'evaluatingAbortSuccess', error:'evaluatingAbortError'},
evaluatingAbortSuccess:{to:{reAttemptGoal:'executingPlan',reEvaluatePlan:'evaluatingThisComponentAsExecutionPlan'}},
evaluatingAbortError:{to:{reAttemptGoal:'executingPlan',reEvaluatePlan:'evaluatingThisComponentAsExecutionPlan'}},
// abandonable HOC
attemptingAbandon:{},
evaluatingAbandonAttempt:{},
evaluatingAbandonProgress:{},
evaluatingAbandonSuccess:{},
evaluatingAbandonError:{},
changingGoal:{},
reAttemptingGoal:{ // retrying
evaluatingAttemptAction:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'evaluatingPlanExecution'}}),
previousState:(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:'previousState'}}),
},
};
keys(DefaultStateComponents).reduce((acc,key)=>{
acc[key] = (props)=>(props.dispatch({type:'userState',payload:key}));
return acc;
},{});
return (
stateComponents={},
dispatchFn=(props)=>(e)=>props[DISPATCH_NAME]({type:'userGoalStateChange',payload:{to:props.goalPath}}),
handlers={
attemptGoal:(props)=>(e)=>{
return Promise.resolve()
.then(props.userEvaluatingProgress)
.then(props.userEvaluatingSuccess)
.catch(props.userEvaluatingError)
},
cancelGoalAttempt:(props)=>(e)=>{
return Promise.resolve()
.then(props.dispatchers.determiningGoal)
},
},
goalStateTreeKey='',
stateTree={}
)=>(DeterminingGoalComponent)=>{
const tree = {};
const states = Object.assign({},DefaultStateComponents,stateComponents);
const dispatchers = keys(stateComponents).reduce((acc,key)=>{
acc[key] = (props)=>(props.dispatch({type:'userState',payload:key}));
return acc;
},{});
const componentChooser = withReducer('userState','dispatch',(state,{type,payload})=>payload,'determiningGoal');
return componentChooser((props)=>{
return React.createElement(stateComponents[props.userState], {...props, ...dispatchers,...handlers});
})
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment