Created
May 26, 2014 19:01
-
-
Save velsa/f126f2e967e7fa93b8ff 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
/** @jsx React.DOM */ | |
// | |
// Parent component must provide the following: | |
// | |
// -- DATA: | |
// | |
// getHashRouterConfig(): { | |
// routes: [ | |
// { | |
// state: 'current_tab', | |
// type: 'number', | |
// default: 0, | |
// max: 4, | |
// onChange: this.tab_changed, | |
// }, | |
// { | |
// state: 'current_user', | |
// type: 'string', | |
// default: 'admin', | |
// valid: [ 'admin', 'guest', 'joe' ], | |
// onChange: this.user_changed, | |
// }, | |
// ... | |
// ], | |
// } | |
// | |
// onChange is called with previous state value. E.g.: | |
// tab_changed: function(old_tab) { | |
// console.debug('Tab changed from', old_tab, 'to', this.state.current_tab) | |
// } | |
// | |
// | |
// | |
// this.props.parentHashRouter: if provided, all path changes will be passed to it, if not, HashRouter will | |
// act as a parent and will set browser's hash according to provided routes | |
// | |
// | |
// -- METHODS: | |
// | |
// hashRouterNavigate(state): use this function to change both required state and hash path | |
// e.g. | |
// this.hashRouterNavigate({current_user: 'guest'}) | |
// | |
// | |
// hashRouterGetHREF(state): returns href, which can be used in anchor elements | |
// e.g. | |
// var href = this.hashRouterGetHREF({current_tab: 2}); | |
// return <a href={href} /> | |
// | |
var HashRouterMixin = { | |
// Our internal state | |
_hrmxn_getInitialState: function(props) { | |
props = props || this.props; | |
var self = this; | |
var res = self._hrmxn_parse_path(props); | |
if (!res.path_was_valid) { | |
setTimeout(function() { | |
if (!self.props.parentHashRouter) { | |
console.debug('HashRouter (root): navigation ', res.state); | |
} else { | |
console.debug('HashRouter (child): navigation ', res.state); | |
} | |
self.hashRouterNavigate(res.state, props); | |
}, 300); | |
} | |
// Expose navigation function, so they can be passed via props to child components | |
res.state.hashRouterNavigate = self.hashRouterNavigate; | |
res.state.hashRouterGetHREF = self.hashRouterGetHREF; | |
return res.state; | |
}, | |
// Copy original data from component's props into our state | |
componentWillMount: function() { | |
var self = this; | |
if (!self.props.parentHashRouter) { | |
window.addEventListener("hashchange", self._hrmxn_browser_hash_changed); | |
} | |
self.setState(self._hrmxn_getInitialState(self.props)); | |
}, | |
componentWillReceiveProps: function(new_props) { | |
var self = this; | |
var new_state = self._hrmxn_getInitialState(new_props); | |
var notifiations = self._hrmxn_prepare_notifications(new_state); | |
self.setState(new_state, function() { | |
// REACT BUG: setState inside componentWillReceiveProps() DOES NOT CHANGE THE STATE ! | |
// IMPORTANT ! REMOVE setTimeout() when they merge the fix into master branch ! | |
setTimeout(function() { | |
notifiations.forEach(function(notifiation, index) { | |
notifiation.callback(notifiation.old_val); | |
}); | |
}, 300); | |
}); | |
}, | |
componentWillUnmount: function () { | |
var self = this; | |
if (!self.props.parentHashRouter) { | |
window.removeEventListener("hashchange", self._hrmxn_browser_hash_changed); | |
} | |
}, | |
_hrmxn_prepare_notifications: function(new_state) { | |
if (!this.state) return; | |
var self = this; | |
var routes = self.getHashRouterConfig().routes; | |
var notifiations = []; | |
routes.forEach(function(route, index) { | |
if ((typeof new_state[route.state] !== 'undefined') && route.onChange) { | |
notifiations.push({ | |
callback: route.onChange, | |
old_val: self.state[route.state], | |
}); | |
} | |
}); | |
return notifiations; | |
}, | |
// Get the current path based on provided state | |
_hrmxn_path_from_state: function(state) { | |
var self = this; | |
var path = ''; | |
var routes = self.getHashRouterConfig().routes; | |
routes.forEach(function(route, index) { | |
if (state && (typeof state[route.state] !== 'undefined')) { | |
path += '/'+state[route.state]; | |
} else if (self.state) { | |
path += '/'+self.state[route.state]; | |
} else { | |
path += '/'+route.default; | |
} | |
}); | |
return path; | |
}, | |
// Get the default path based on provided routes | |
_hrmxn_default_path: function() { | |
var path = ''; | |
var routes = this.getHashRouterConfig().routes; | |
routes.forEach(function(route, index) { | |
path += '/'+route.default; | |
}); | |
return path; | |
}, | |
// | |
// Change browser's location hash | |
// | |
hashRouterNavigate: function(state, props) { | |
props = props || this.props; | |
var self = this; | |
if (!props.parentHashRouter) { | |
location.hash = '#'+self._hrmxn_path_from_state(state)+(state._hrmxn_subPath || ''); | |
console.debug('HashRouter (root): hashRouterNavigate: ', location.hash, state, self.state); | |
} else { | |
props.parentHashRouter.hashRouterNavigate({ | |
_hrmxn_subPath: self._hrmxn_path_from_state(state)+(state._hrmxn_subPath || ''), | |
});//.bind(props.parentHashRouter); | |
console.debug('HashRouter (child): hashRouterNavigate: '+self._hrmxn_path_from_state(state)+state._hrmxn_subPath); | |
} | |
}, | |
// | |
// Change browser's location hash | |
// | |
hashRouterGetHREF: function(state) { | |
var self = this; | |
if (!self.props.parentHashRouter) { | |
return '#'+self._hrmxn_path_from_state(state); | |
} else { | |
return self.props.parentHashRouter.hashRouterGetHREF(null) + self._hrmxn_path_from_state(state); | |
} | |
}, | |
// | |
// React on browser's location hash change | |
// | |
_hrmxn_browser_hash_changed: function() { | |
var self = this; | |
var new_state = self._hrmxn_getInitialState(self.props); | |
self.setState(new_state); | |
}, | |
// | |
// Helpers | |
// | |
_hrmxn_parse_path: function(props) { | |
props = props || this.props; | |
var self = this; | |
var path; | |
if (!props.parentHashRouter) { | |
path = location.hash.slice(1); | |
} else { | |
path = props.parentHashRouter._hrmxn_subPath; | |
} | |
var routes = self.getHashRouterConfig().routes; | |
if (!routes) { | |
console.error('HashRouter: getHashRouterConfig returned null ?'); | |
return null; | |
} | |
var is_valid = true; | |
if (!path) { | |
console.debug('HashRouter: got NULL path, using defaults...'); | |
is_valid = false; | |
path = self._hrmxn_default_path(); | |
} | |
var i; | |
// Build the appropriate regexp for matching our path and possible subpath | |
var regex_str = '^\\/'; | |
for (i=0; i < routes.length; i++) { | |
if (routes[i].type === 'number') regex_str += '([0-9]+)'; | |
else if (routes[i].type === 'string') regex_str += '([a-zA-Z_][a-zA-Z0-9_]+)'; | |
else { | |
console.error('HashRouter: route '+routes[i].state+' ('+i+') type "'+routes[i].type+'" is not supported !'); | |
return null; | |
} | |
if (i !== (routes.length-1)) { | |
regex_str += '\\/'; | |
} | |
} | |
// Catch subPath | |
regex_str += '(.*)?$'; | |
var regex = new RegExp(regex_str); | |
var m = path.match(regex); | |
if (!m) { | |
console.debug('HashRouter: matching failed for: '+path); | |
is_valid = false; | |
path = self._hrmxn_default_path(); | |
m = path.match(regex); | |
if (!m) { | |
console.error('HashRouter: matching failed for default path ('+path+') ? '); | |
return null; | |
} | |
} | |
var valid_value = function(val, route) { | |
if (route.type === 'number') { | |
var def_num_val = typeof route.default === 'undefined' ? 0 : route.default; | |
// Validation for numbers | |
if (typeof val !== 'number') { | |
num_val = parseInt(val, 10); | |
if (isNaN(num_val)) { | |
console.debug('HashRouter: non numeric value', val, route); | |
is_valid = false; | |
return def_num_val; | |
} else { | |
val = num_val; | |
} | |
} | |
if (route.max) { | |
if (val > route.max) { | |
console.debug('HashRouter: value > max', val, route); | |
is_valid = false; | |
return def_num_val; | |
} | |
} | |
return val; | |
} else if (route.type === 'string') { | |
// Validation for strings | |
if (route.valid) { | |
if (route.valid.indexOf(val) < 0) { | |
console.debug('HashRouter: value invalid', val, route); | |
is_valid = false; | |
return route.default; | |
} | |
} | |
return val; | |
} | |
}; | |
// Check each route against found match | |
var new_state = {}; | |
routes.forEach(function(route, index) { | |
new_state[route.state] = valid_value(m[index+1], route); | |
}); | |
new_state._hrmxn_subPath = m[routes.length+1]; | |
if (typeof new_state._hrmxn_subPath === 'undefined') new_state._hrmxn_subPath = ''; | |
if (!self.props.parentHashRouter) { | |
console.debug('HashRouter (root): parsed ', new_state, is_valid); | |
} else { | |
console.debug('HashRouter (child): parsed ', new_state, is_valid); | |
} | |
return { | |
state: new_state, | |
path_was_valid: is_valid, | |
}; | |
}, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment