Skip to content

Instantly share code, notes, and snippets.

@velsa
Created May 26, 2014 19:01
Show Gist options
  • Save velsa/f126f2e967e7fa93b8ff to your computer and use it in GitHub Desktop.
Save velsa/f126f2e967e7fa93b8ff to your computer and use it in GitHub Desktop.
/** @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