Last active
August 29, 2015 14:04
-
-
Save slorber/096953f247ba2c744057 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
'use strict'; | |
var React = require("react/addons"); | |
var Q = require("q"); | |
var Main = require("components/layout/main"); | |
var CurrentUserService = require("services/currentUserService"); | |
var ApiRequestSessionHolder = require("repositories/utils/apiRequestSessionHolder"); | |
var AppState = require("model/appState"); | |
var Atom = require("state/atom/atom"); | |
var SelectedSpaceModel = require("model/selectedSpaceModel"); | |
var ViewedSpaceModel = require("model/viewedSpaceModel"); | |
var TimelineStoreManager = require("state/stores/timelineStoreManager") | |
var TopLevelScreenStoreManager = require("state/stores/topLevelScreenStoreManager") | |
var CategoryNavigationStoreManager = require("state/stores/categoryNavigationStoreManager") | |
var GlobalEventBus = require("common/globalEventBus"); | |
var AppEvents = require("appEvents"); | |
var AppRouter = require("appRouter"); | |
var app = { | |
initialize: function() { | |
Q.longStackSupport = true; | |
// TODO in next versions of React it was said to be on by default, so probably deprecated | |
React.initializeTouchEvents(true); | |
document.addEventListener('deviceready', this.onDeviceReady.bind(this) , false); | |
}, | |
onDeviceReady: function() { | |
var self = this; | |
var initialState = new AppState(); | |
initialState.routing = {}; | |
initialState.stores = {}; | |
this.appStateAtom = new Atom({ | |
initialState: initialState, | |
onChange: this.onAtomChange.bind(this), | |
reactToChange: this.reactToAtomChange.bind(this) | |
}); | |
this.appRouter = new AppRouter(this.appStateAtom); | |
// TODO the stores should probably register to the atom themselves... | |
this.timelineStoreManager = new TimelineStoreManager(this.appStateAtom,["stores","timelineStore"]); | |
this.topLevelScreenStoreManager = new TopLevelScreenStoreManager(this.appStateAtom,["stores","topLevelScreenStore"]); | |
this.categoryNavigationStoreManager = new CategoryNavigationStoreManager(this.appStateAtom,["stores","categoryNavigationStore"]); | |
this.appStateAtom.cursor().follow("authenticatedUser").setAsyncValue( CurrentUserService.autoLogin() ); | |
GlobalEventBus.subscribe(function(event) { | |
self.handleEvent(event) | |
}); | |
}, | |
// TODO bad should be part of router code | |
handleEvent: function(event) { | |
console.debug("Event:",event); | |
switch (event.name) { | |
case AppEvents.Names.OPEN_TOP_LEVEL_SCREEN: | |
this.appStateAtom.cursor().follow("topLevelScreen").set(event.data.screen); | |
break; | |
case AppEvents.Names.CLOSE_TOP_LEVEL_SCREEN: | |
this.appStateAtom.cursor().follow("topLevelScreen").unset(); | |
break; | |
} | |
// TODO bad but temporary | |
this.timelineStoreManager.handleEvent(event); | |
this.topLevelScreenStoreManager.handleEvent(event); | |
this.categoryNavigationStoreManager.handleEvent(event); | |
}, | |
onAtomChange: function() { | |
this.render(); | |
}, | |
// TODO this probably can be done in a better and easier way | |
reactToAtomChange: function(previousState) { | |
if ( this.userHasLoggedIn(previousState) ) { | |
this.appStateAtom.setPathValue(["routing","selectedPane"],"Stamples"); | |
this.appStateAtom.setPathValue(["routing","selectedSpace"],SelectedSpaceModel.forAll()); | |
this.appStateAtom.setPathValue(["routing","viewedSpace"],ViewedSpaceModel.forRoot()); | |
console.debug("User has just logged in, atom updated",this.appStateAtom.get()); | |
} | |
// TODO store managers should subscribe on events and route change | |
this.timelineStoreManager.reactToChange(previousState); | |
this.topLevelScreenStoreManager.reactToChange(previousState); | |
this.categoryNavigationStoreManager.reactToChange(previousState); | |
}, | |
userHasLoggedIn: function(previousState) { | |
var state = this.appStateAtom.get(); | |
var authenticatedUserIsModified = (state.authenticatedUser && previousState.authenticatedUser && state.authenticatedUser !== previousState.authenticatedUser); | |
if ( authenticatedUserIsModified ) { | |
var wasLoading = previousState.authenticatedUser.isLoading(); | |
var isNowLoaded = state.authenticatedUser.isSuccess(); | |
return wasLoading && isNowLoaded; | |
} | |
return false; | |
}, | |
render: function() { | |
var self = this; | |
if ( this.appStateAtom.get().currentUser ) { | |
// This is required to automatically perform authenticated requests | |
// without having to configure the request headers every time... | |
ApiRequestSessionHolder.setApiRequestSession(this.appStateAtom.get().currentUser.sessionToken); | |
} | |
var props = { | |
appStateCursor: this.appStateAtom.cursor(), | |
appRouter: this.appRouter | |
}; | |
if ( !this.appComponent ) { | |
var mountNode = document.getElementById('reactAppContainer'); | |
var mountComponent = Main(props); | |
console.debug("Rendering main component",this.appStateAtom.get()); | |
this.appComponent = React.renderComponent(mountComponent,mountNode); | |
} | |
else { | |
console.debug("Re-rendering main component",this.appStateAtom.get()); | |
this.appComponent.setProps(props); | |
} | |
} | |
}; | |
app.initialize(); | |
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
'use strict'; | |
var React = require("react/addons"); | |
var DeepFreeze = require("common/deepFreeze"); | |
var AtomUtils = require("state/atom/atomUtils"); | |
var AtomCursor = require("state/atom/atomCursor"); | |
function noop() { } // Convenient but probably not performant: TODO ? | |
/** | |
* Creates an Atom | |
* It contains an immutable state that is never modified directly, but can be swapped to a new immutable state | |
* @param options | |
* @constructor | |
*/ | |
var Atom = function Atom(options) { | |
this.state = options.initialState || {}; | |
this.onChange = options.onChange || noop; | |
this.reactToChange = options.reactToChange || noop; | |
DeepFreeze(this.state); | |
}; | |
/** | |
* Change the state reference hold in this Atom | |
* @param newState | |
*/ | |
Atom.prototype.set = function(newState) { | |
if ( newState === this.state ) { | |
console.debug("Atom state did not change") | |
return; // Nothing to do here, the atom doesn't change its state | |
} | |
var previousState = this.state; | |
this.state = newState; | |
DeepFreeze(this.state); | |
if ( !this.currentlyReactingToChanges ) { | |
this.currentlyReactingToChanges = true; | |
try { | |
this.reactToChange(previousState); | |
} catch ( error ) { | |
throw error; // TODO do something more clever? | |
} finally { | |
this.currentlyReactingToChanges = false; | |
} | |
} | |
scheduleOnChange(this) | |
}; | |
/** | |
* Get the current state of the Atom | |
* @return the Atom state | |
*/ | |
Atom.prototype.get = function() { | |
return this.state; | |
}; | |
/** | |
* Get a cursor,, that permits to focus on a given path of the Atom | |
* @param path (defaults to atom root cursor) | |
* @return {AtomCursor} | |
*/ | |
Atom.prototype.cursor = function(path) { | |
path = path || []; | |
return new AtomCursor(this,path); | |
}; | |
/** | |
* Change the value at a given path of the atom | |
* @param path | |
* @param value | |
*/ | |
Atom.prototype.setPathValue = function(path,value) { | |
var newState = AtomUtils.updatePathValue(this.state,path,value); | |
this.set(newState); | |
}; | |
Atom.prototype.unsetPathValue = function(path) { | |
// TODO find a better thing than "undefined" to set | |
var newState = AtomUtils.updatePathValue(this.state,path,undefined); | |
this.set(newState); | |
}; | |
/** | |
* Get the value at a given path of the atom | |
* @param path | |
* @return value | |
*/ | |
Atom.prototype.getPathValue = function(path) { | |
return AtomUtils.getPathValue(this.state,path); | |
}; | |
/** | |
* Compare and swap a valua at a given path of the atom | |
* @param path | |
* @param expectedValue | |
* @param newValue | |
* @return true if the CAS operation was successful | |
*/ | |
Atom.prototype.compareAndSwapPathValue = function(path,expectedValue,newValue) { | |
var actualValue = this.getPathValue(path); | |
if ( actualValue === expectedValue ) { | |
this.setPathValue(path,newValue); | |
return true; | |
} | |
return false; | |
}; | |
// We fire the onChange callback only once | |
// even if has been modified multiple time during the same execution context | |
function scheduleOnChange(atom) { | |
if ( !atom.onChangeTimer ) { | |
atom.onChangeTimer = setTimeout( | |
(function() { | |
delete atom.onChangeTimer; | |
atom.onChange(); | |
}).bind(this) | |
,0 | |
); | |
} | |
}; | |
module.exports = Atom; |
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
'use strict'; | |
var _ = require("lodash"); | |
var AtomAsyncValueStates = { | |
LOADIND: {value:"LOADIND"}, | |
SUCCESS: {value:"SUCCESS"}, | |
ERROR: {value:"ERROR"} | |
} | |
exports.AtomAsyncValueStates = AtomAsyncValueStates; | |
var AtomAsyncValue = function(state) { | |
this.state = state || AtomAsyncValueStates.LOADIND; | |
}; | |
AtomAsyncValue.prototype.isLoading = function() { | |
return this.state === AtomAsyncValueStates.LOADIND; | |
}; | |
AtomAsyncValue.prototype.isSuccess = function() { | |
return this.state === AtomAsyncValueStates.SUCCESS; | |
}; | |
AtomAsyncValue.prototype.isError = function() { | |
return this.state === AtomAsyncValueStates.ERROR; | |
}; | |
AtomAsyncValue.prototype.toSuccess = function(value) { | |
var async = new AtomAsyncValue(AtomAsyncValueStates.SUCCESS); | |
async.value = value; | |
return async; | |
}; | |
AtomAsyncValue.prototype.asSuccess = function() { | |
if ( !this.isSuccess() ) throw new Error("Can't convert async value as success becauuse its state is " + this.state); | |
return this.value; | |
}; | |
AtomAsyncValue.prototype.toError = function(error) { | |
var async = new AtomAsyncValue(AtomAsyncValueStates.ERROR); | |
async.error = error; | |
return async; | |
}; | |
AtomAsyncValue.prototype.asError = function() { | |
if ( !this.isError() ) throw new Error("Can't convert async value as error becauuse its state is " + this.state); | |
return this.error; | |
}; | |
exports.AtomAsyncValue = AtomAsyncValue; | |
function setupAsyncValueSwapping(atom,path,asyncValue,promise) { | |
promise | |
.then(function asyncCompletionSuccess(data) { | |
var swapped = atom.compareAndSwapPathValue(path,asyncValue,asyncValue.toSuccess(data)); | |
console.debug("Async value completion",path,"Swap success=",swapped); | |
}) | |
.fail(function asyncCompletionError(error) { | |
var swapped = atom.compareAndSwapPathValue(path,asyncValue,asyncValue.toError(error)); | |
console.error("Async value completion error",path,"Swap success=",swapped); | |
console.error(error.stack); | |
}) | |
.done(); | |
}; | |
function setPathAsyncValue(atom,path,promise) { | |
var asyncValue = new AtomAsyncValue(); | |
atom.setPathValue(path,asyncValue); | |
setupAsyncValueSwapping(atom,path,asyncValue,promise); | |
}; | |
exports.setPathAsyncValue = setPathAsyncValue; | |
function setPathResolvedAsyncValue(atom,path,resolvedValue) { | |
var asyncValue = new AtomAsyncValue().toSuccess(resolvedValue); | |
atom.setPathValue(path,asyncValue); | |
}; | |
exports.setPathResolvedAsyncValue = setPathResolvedAsyncValue; | |
function pushPathAsyncValue(atom,listPath,promise) { | |
var list = atom.getPathValue(listPath) || []; | |
if ( list instanceof Array ) { | |
var asyncValueIndex = list.length; | |
var asyncValuePath = listPath.concat([asyncValueIndex]); | |
var asyncValue = new AtomAsyncValue(); | |
list.push(asyncValue); | |
atom.setPathValue(listPath,list); | |
setupAsyncValueSwapping(atom,asyncValuePath,asyncValue,promise); | |
} else { | |
throw new Error("Can't push async value in list because list is " + JSON.stringify(list)); | |
} | |
}; | |
exports.pushPathAsyncValue = pushPathAsyncValue; | |
function getPathAsyncValueListCursors(atom,listPath) { | |
var asyncValueList = atom.getPathValue(listPath); | |
if ( asyncValueList instanceof Array ) { | |
var cursorsArray = asyncValueList.map(function(asyncValue,asyncValueIndex) { | |
if ( asyncValue.isSuccess() ) { | |
if ( asyncValue.value instanceof Array ) { | |
return asyncValue.value.map(function(asyncValueItem,asyncValueItemIndex) { | |
var cursorPath = listPath.concat([asyncValueIndex,"value",asyncValueItemIndex]); | |
return atom.cursor(cursorPath); | |
}) | |
} else { | |
return [asyncValue.value] | |
} | |
} else { | |
return []; | |
} | |
}); | |
return _.flatten(cursorsArray); // TODO maybe remove dependency to underscore | |
} else { | |
throw new Error("can only be called on an array, not a " + asyncValueList); | |
} | |
} | |
exports.getPathAsyncValueListCursors = getPathAsyncValueListCursors; | |
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
'use strict'; | |
var _ = require("lodash"); | |
var AtomAsyncUtils = require("state/atom/atomAsyncUtils"); | |
var AtomCursor = function AtomCursor(atom,atomPath) { | |
this.atom = atom; | |
this.atomPath = atomPath; | |
this.atomValue = atom.getPathValue(atomPath); // TODO this probably can be optimized in some cases when navigating from a previous cursor | |
}; | |
function ensureIsArray(maybeArray,message) { | |
if ( !(maybeArray instanceof Array) ) { | |
throw new Error("Not an array: " + maybeArray + " -> " + message); | |
} | |
} | |
AtomCursor.prototype.get = function() { | |
return this.atomValue; | |
}; | |
AtomCursor.prototype.set = function(value) { | |
this.atom.setPathValue(this.atomPath,value); | |
}; | |
AtomCursor.prototype.unset = function() { | |
this.atom.unsetPathValue(this.atomPath); | |
}; | |
AtomCursor.prototype.push = function(value) { | |
var list = this.atomValue || []; | |
ensureIsArray(list,"can only call push on an array"); | |
var newList = list.concat([value]); | |
this.atom.setPathValue(this.atomPath,newList); | |
}; | |
AtomCursor.prototype.without = function(value) { | |
var list = this.atomValue; | |
ensureIsArray(list,"can only call without on an array"); | |
var newList = _.without(list,value); | |
this.atom.setPathValue(this.atomPath,newList); | |
}; | |
AtomCursor.prototype.update = function(updateFunction) { | |
if ( !this.atomValue ) throw new Error("you can't update an unexisting value") | |
var valueToSet = updateFunction(this.atomValue); | |
this.atom.setPathValue(this.atomPath,valueToSet); | |
}; | |
AtomCursor.prototype.plus = function(number) { | |
this.update(function(value) { return value+number }); | |
}; | |
AtomCursor.prototype.minus = function(number) { | |
this.update(function(value) { return value-number }); | |
}; | |
AtomCursor.prototype.followPath = function(path) { | |
var newPath = this.atomPath.concat(path); | |
return new AtomCursor(this.atom,newPath); | |
}; | |
AtomCursor.prototype.follow = function(pathElement) { | |
return this.followPath( [pathElement] ); | |
}; | |
AtomCursor.prototype.list = function() { | |
var list = this.atomValue; | |
ensureIsArray(list,"can only call list on an array"); | |
return list.map(function(item,index) { | |
return this.follow(index); | |
}.bind(this)); | |
}; | |
AtomCursor.prototype.asyncSuccess = function() { | |
if ( this.atomValue && this.atomValue.isSuccess() ) { | |
return this.follow("value"); | |
} else { | |
throw new Error("You can't follow an async value that is not successfully loaded: "+this.atomValue); | |
} | |
}; | |
AtomCursor.prototype.asyncList = function() { | |
return AtomAsyncUtils.getPathAsyncValueListCursors(this.atom,this.atomPath); | |
}; | |
AtomCursor.prototype.setAsyncValue = function(promise) { | |
AtomAsyncUtils.setPathAsyncValue(this.atom,this.atomPath,promise); | |
}; | |
AtomCursor.prototype.pushAsyncValue = function(promise) { | |
AtomAsyncUtils.pushPathAsyncValue(this.atom,this.atomPath,promise); | |
}; | |
module.exports = AtomCursor; |
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
'use strict'; | |
var React = require("react/addons"); | |
function pathToReactUpdatePath(path,objectAtPath) { | |
if ( path.length == 0 ) { | |
return objectAtPath; | |
} else { | |
var head = path[0]; | |
var tail = path.slice(1); | |
var result = {}; | |
result[head] = pathToReactUpdatePath(tail,objectAtPath); | |
return result; | |
} | |
}; | |
function getPathValue(object,path) { | |
if ( path.length == 0 ) { | |
return object; | |
} else { | |
var head = path[0]; | |
var tail = path.slice(1); | |
var headValue = object[head]; | |
if ( headValue ) { | |
return getPathValue(headValue,tail); | |
} else { | |
return undefined; | |
} | |
} | |
}; | |
exports.getPathValue = getPathValue; | |
function updatePathValue(object,path,value) { | |
try { | |
var objectPath = pathToReactUpdatePath(path,{$set: value}); | |
return React.addons.update(object, objectPath); | |
} catch (error) { | |
// TODO we should probably create the missing path instead of raison the exception | |
throw new Error( | |
"Can't set value " + JSON.stringify(value) + | |
" at path " + JSON.stringify(path) + | |
" because of error: " + error.message | |
); | |
} | |
}; | |
exports.updatePathValue = updatePathValue; | |
function modifiedKeys(objectBefore,objectAfter) { | |
if ( objectBefore === objectAfter ) { | |
return []; | |
} | |
else { | |
// Merge and deduplicate keys | |
var keysToCompare = _.union( | |
_.keys(objectBefore), | |
_.keys(objectAfter) | |
); | |
// Return only modified keys by reference equality check between the 2 objects | |
return keysToCompare.filter(function(key) { | |
return objectBefore[key] === objectAfter[key]; | |
}) | |
} | |
}; | |
exports.modifiedKeys = modifiedKeys; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment